cryptnox-sdk-arduino 1.0.0
Arduino library for Cryptnox Hardware Wallet
Loading...
Searching...
No Matches
CW_SecureChannel.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
18
19#include "CW_SecureChannel.h"
20#include "CW_Utils.h"
21#include "CW_TrustedKeys.h"
22
23/******************************************************************
24 * Module-level constants
25 ******************************************************************/
26
27#define RESPONSE_GETCARDCERTIFICATE_IN_BYTES 148U
28/* SELECT AID: 1 (type) + 3 (ver) + 32 (status) + 2 (SW) = 38 bytes */
29#define RESPONSE_SELECT_IN_BYTES 40U
30/* GET_MANUFACTURER_CERT: full DataOut up to certLen(2)+cert(411)+SW(2)=415 bytes; 420 for margin. */
31#define RESPONSE_GETMANUFACTURERCERT_PAGE_IN_BYTES 420U
32#define RESPONSE_OPENSECURECHANNEL_IN_BYTES 34U
33#define REQUEST_MUTUALLYAUTHENTICATE_IN_BYTES 69U
34#define RESPONSE_MUTUALLYAUTHENTICATE_IN_BYTES 66U
35#define RESPONSE_STATUS_WORDS_IN_BYTES 2U
36
37#define OPENSECURECHANNEL_SALT_IN_BYTES (RESPONSE_OPENSECURECHANNEL_IN_BYTES - RESPONSE_STATUS_WORDS_IN_BYTES)
38#define GETCARDCERTIFICATE_IN_BYTES (RESPONSE_GETCARDCERTIFICATE_IN_BYTES - RESPONSE_STATUS_WORDS_IN_BYTES)
39
40#define RANDOM_BYTES 8U
41#define COMMON_PAIRING_DATA CW_PAIRING_DATA
42#define CLIENT_PRIVATE_KEY_SIZE 32U
43#define CLIENT_PUBLIC_KEY_SIZE 64U
44#define CARDEPHEMERALPUBKEY_SIZE 64U
45#define AES_BLOCK_SIZE 16U
46#define APDU_HEADER_LEN (4U)
47#define APDU_LC_LEN (1U)
48#define MAC_APDU_LEN (12U)
49#define INPUT_BUFFER_LIMIT (CW_USER_DATA_PAGE_SIZE)
50#define ENC_BUF_MAX_LEN (INPUT_BUFFER_LIMIT + AES_BLOCK_SIZE)
51#define MAX_MAC_DATA_LEN (APDU_HEADER_LEN + MAC_APDU_LEN + ENC_BUF_MAX_LEN)
52#define SEND_APDU_MAX_LEN (APDU_HEADER_LEN + APDU_LC_LEN + AES_BLOCK_SIZE + ENC_BUF_MAX_LEN)
53
54/* Enforce APDU fits within a single PN532 APDU (255 bytes max) */
56 "CW_USER_DATA_PAGE_SIZE too large for PN532 single APDU transport");
57
58/* Shared static crypto scratch buffers — reuse is safe because decrypt is
59 * always called from inside encrypt AFTER encrypt's large buffers are done. */
60static uint8_t s_apduBuf[SEND_APDU_MAX_LEN]; /* 245 bytes */
61static uint8_t s_macBuf [MAX_MAC_DATA_LEN]; /* 240 bytes */
62static uint8_t s_dataBuf[ENC_BUF_MAX_LEN]; /* 224 bytes */
63
64/* Manufacturer certificate assembly buffer (used only during verifyCertificateChain). */
66
67/* DER TLV tag bytes */
68#define DER_TAG_SEQUENCE (0x30U) /* SEQUENCE (universal, constructed) */
69#define DER_TAG_BIT_STRING (0x03U) /* BIT STRING */
70#define DER_TAG_CTX0 (0xA0U) /* [0] EXPLICIT — version in v3 TBSCertificate */
71
72/* DER length-field encoding */
73#define DER_LEN_LONG_FLAG (0x80U) /* set = long-form length */
74#define DER_LEN_LONG_1 (0x81U) /* long form, 1 following byte */
75#define DER_LEN_LONG_2 (0x82U) /* long form, 2 following bytes */
76
77/* EC-point encoding */
78#define DER_EC_UNCOMPRESSED (0x04U) /* uncompressed point prefix */
79#define DER_EC_POINT_BYTES (65U) /* 0x04 || X[32] || Y[32] */
80#define DER_BIT_UNUSED_ZERO (0x00U) /* BIT STRING unused-bits field must be 0 */
81
82/******************************************************************
83 * Constructor
84 ******************************************************************/
85
86// cppcheck-suppress misra-c2012-12.3 -- C++: member initializer-list commas are not the comma operator
88 CW_Logger& logger,
89 CW_CryptoProvider& crypto,
91 : _driver(driver), _logger(logger), _crypto(crypto), _platform(platform),
93 memset(_lastNonce, 0, sizeof(_lastNonce));
94}
95
96/******************************************************************
97 * Transport delegation methods
98 ******************************************************************/
99
101 return _driver.begin();
102}
103
105 return _driver.inListPassiveTarget();
106}
107
109 _driver.resetReader();
110}
111
113 return _driver.printFirmwareVersion();
114}
115
116/******************************************************************
117 * Private helpers
118 ******************************************************************/
119
120bool CW_SecureChannel::checkStatusWord(const uint8_t* response, uint16_t responseLength,
121 uint8_t sw1Expected, uint8_t sw2Expected) {
122 bool ret = false;
123
124 if ((response == NULL) || (responseLength < 2U)) {
125#if CW_DEBUG_LOGGING
126 _logger.println(F("checkStatusWord: response too short."));
127#endif
128 }
129 else {
130 uint8_t sw1 = response[responseLength - 2U];
131 uint8_t sw2 = response[responseLength - 1U];
132
133 if ((sw1 == sw1Expected) && (sw2 == sw2Expected)) {
134 ret = true;
135 }
136 else {
137#if CW_DEBUG_LOGGING
138 _logger.print(F("SW: 0x"));
139 if (sw1 < 16U) { _logger.print(F("0")); }
140 _logger.print(sw1, HEX);
141 _logger.print(F(" 0x"));
142 if (sw2 < 16U) { _logger.print(F("0")); }
143 _logger.println(sw2, HEX);
144#endif
145 }
146 }
147
148 return ret;
149}
150
151/******************************************************************
152 * Public methods
153 ******************************************************************/
154
156 bool ret = false;
157
158 uint8_t selectApduCmd[] = {
159 0x00, 0xA4, 0x04, 0x00,
160 0x07,
161 0xA0, 0x00, 0x00, 0x10, 0x00, 0x01, 0x12
162 };
163
164 uint8_t response[RESPONSE_SELECT_IN_BYTES];
165 uint8_t responseLength = static_cast<uint8_t>(sizeof(response));
166
167 if (_driver.sendAPDU(selectApduCmd, sizeof(selectApduCmd), response, responseLength)) {
168 if (checkStatusWord(response, responseLength, 0x90U, 0x00U)) {
169 ret = true;
170 } else {
171#if CW_DEBUG_LOGGING
172 _logger.println(F("Select APDU failed."));
173#endif
174 }
175 } else {
176#if CW_DEBUG_LOGGING
177 _logger.println(F("APDU select failed."));
178#endif
179 }
180
181 return ret;
182}
183
184bool CW_SecureChannel::getCardCertificate(uint8_t* cardCertificate, uint8_t& cardCertificateLength) {
185 bool ret = false;
186 uint8_t getCardCertificateResponse[RESPONSE_GETCARDCERTIFICATE_IN_BYTES];
187 uint8_t getCardCertificateResponseLength = static_cast<uint8_t>(sizeof(getCardCertificateResponse));
188
189 if (cardCertificate != NULL) {
190 uint8_t randomBytes[RANDOM_BYTES] = { 0U };
191 if (!_crypto.random(randomBytes, RANDOM_BYTES)) {
192#if CW_DEBUG_LOGGING
193 _logger.println(F("getCardCertificate: RNG failed."));
194#endif
195 return false;
196 }
197
198 /* Store nonce for replay check in verifyCertificateChain(). */
200
201 uint8_t getCardCertificateApdu[] = {
202 0x80, 0xF8, 0x00, 0x00, 0x08
203 };
204
205 uint8_t fullApdu[sizeof(getCardCertificateApdu) + RANDOM_BYTES];
206 (void)CW_Utils::safe_memcpy(fullApdu, sizeof(fullApdu), getCardCertificateApdu, sizeof(getCardCertificateApdu));
207 (void)CW_Utils::safe_memcpy(fullApdu + sizeof(getCardCertificateApdu), RANDOM_BYTES, randomBytes, RANDOM_BYTES);
208
209 if (_driver.sendAPDU(fullApdu, sizeof(fullApdu),
210 getCardCertificateResponse, getCardCertificateResponseLength)) {
211 if (checkStatusWord(getCardCertificateResponse, getCardCertificateResponseLength,
212 0x90U, 0x00U)) {
213 cardCertificateLength = static_cast<uint8_t>(
214 static_cast<uint8_t>(getCardCertificateResponseLength - RESPONSE_STATUS_WORDS_IN_BYTES));
215 (void)CW_Utils::safe_memcpy(cardCertificate, GETCARDCERTIFICATE_IN_BYTES, getCardCertificateResponse, cardCertificateLength);
216 ret = true;
217 } else {
218#if CW_DEBUG_LOGGING
219 _logger.println(F("getCardCertificate: bad SW."));
220#endif
221 }
222 } else {
223#if CW_DEBUG_LOGGING
224 _logger.println(F("getCardCertificate APDU failed."));
225#endif
226 }
227 }
228
229 return ret;
230}
231
232bool CW_SecureChannel::extractCardEphemeralKey(const uint8_t* cardCertificate,
233 uint8_t* cardEphemeralPubKey,
234 uint8_t* fullEphemeralPubKey65) {
235 bool ret = false;
236
237 if ((cardCertificate == NULL) || (cardEphemeralPubKey == NULL)) {
238 ret = false;
239 }
240 else {
241 const uint8_t keyStart = 1U + 8U; /* skip 'C' and nonce */
242 const uint8_t fullKeyLength = 65U;
243
244 /* Reject any key that is not an uncompressed point (0x04 prefix). */
245 if (cardCertificate[keyStart] != 0x04U) {
246 ret = false;
247 }
248 else {
249 for (uint8_t i = 0U; i < fullKeyLength; i++) {
250 uint8_t b = cardCertificate[keyStart + i];
251 if (fullEphemeralPubKey65 != NULL) {
252 fullEphemeralPubKey65[i] = b;
253 }
254 if (i > 0U) {
255 cardEphemeralPubKey[i - 1U] = b;
256 }
257 }
258 ret = true;
259 }
260 }
261
262 return ret;
263}
264
266 uint8_t* sessionPublicKey,
267 uint8_t* sessionPrivateKey,
268 CW_Curve sessionCurve) {
269 bool ret = false;
270
271 if (!_crypto.makeKey(sessionPublicKey, sessionPrivateKey, sessionCurve)) {
272#if CW_DEBUG_LOGGING
273 _logger.println(F("ECC key generation failed."));
274#endif
275 }
276 else {
277 uint8_t opcApduHeader[] = {
278 0x80, 0x10, 0x00, 0x00, 0x41, 0x04
279 };
280
281 uint8_t fullApdu[sizeof(opcApduHeader) + CLIENT_PUBLIC_KEY_SIZE];
282 (void)CW_Utils::safe_memcpy(fullApdu, sizeof(fullApdu), opcApduHeader, sizeof(opcApduHeader));
283 (void)CW_Utils::safe_memcpy(fullApdu + sizeof(opcApduHeader), CLIENT_PUBLIC_KEY_SIZE, sessionPublicKey, CLIENT_PUBLIC_KEY_SIZE);
284
285 uint8_t response[RESPONSE_OPENSECURECHANNEL_IN_BYTES];
286 uint8_t responseLength = static_cast<uint8_t>(sizeof(response));
287
288 if (_driver.sendAPDU(fullApdu, sizeof(fullApdu), response, responseLength)) {
289 if (checkStatusWord(response, responseLength, 0x90U, 0x00U)) {
290 if (responseLength == static_cast<uint8_t>(RESPONSE_OPENSECURECHANNEL_IN_BYTES)) {
292 ret = true;
293 } else {
294#if CW_DEBUG_LOGGING
295 _logger.println(F("OpenSecureChannel: unexpected response size."));
296#endif
297 }
298 } else {
299#if CW_DEBUG_LOGGING
300 _logger.println(F("OpenSecureChannel: bad SW."));
301#endif
302 }
303 } else {
304#if CW_DEBUG_LOGGING
305 _logger.println(F("OpenSecureChannel APDU failed."));
306#endif
307 }
308 }
309
310 return ret;
311}
312
335 const uint8_t* salt,
336 uint8_t* clientPublicKey,
337 const uint8_t* clientPrivateKey,
338 CW_Curve sessionCurve,
339 const uint8_t* cardEphemeralPubKey) {
340 bool ret = false;
341 uint8_t sharedSecret[32U] = { 0U };
342 (void)clientPublicKey;
343
344 if (!_crypto.ecdh(cardEphemeralPubKey, clientPrivateKey, sharedSecret, sessionCurve)) {
345#if CW_DEBUG_LOGGING
346 _logger.println(F("ECDH failed."));
347#endif
348 }
349 else {
350 uint8_t concat[32U + sizeof(COMMON_PAIRING_DATA) - 1U + 32U] = { 0U };
351 uint8_t sha512Output[64U] = { 0U };
352 const size_t pairingKeyLen = sizeof(COMMON_PAIRING_DATA) - 1U;
353 const size_t concatLen = 32U + pairingKeyLen + 32U;
354
355 (void)CW_Utils::safe_memcpy(concat, sizeof(concat), sharedSecret, 32U);
356 (void)CW_Utils::safe_memcpy(concat + 32U, sizeof(concat) - 32U, reinterpret_cast<const uint8_t*>(COMMON_PAIRING_DATA), pairingKeyLen);
357 (void)CW_Utils::safe_memcpy(concat + 32U + pairingKeyLen, 32U, salt, 32U);
358
359 bool sha512Ok = _crypto.sha512(concat, concatLen, sha512Output);
360
361 if (!sha512Ok) {
362 CW_Utils::secure_wipe(sharedSecret, sizeof(sharedSecret));
363 CW_Utils::secure_wipe(sha512Output, sizeof(sha512Output));
364 CW_Utils::secure_wipe(concat, sizeof(concat));
365 return false;
366 }
367
368 (void)CW_Utils::safe_memcpy(session.aesKey, CW_AESKEY_SIZE, sha512Output, CW_AESKEY_SIZE);
370
371 uint8_t iv_opc[AES_BLOCK_SIZE] = { 0U };
372 uint8_t mac_iv[AES_BLOCK_SIZE] = { 0U };
373 memset(iv_opc, 0x01U, AES_BLOCK_SIZE);
374
375 uint8_t RNG_data[32U] = { 0U };
376 if (!_crypto.random(RNG_data, sizeof(RNG_data))) {
377#if CW_DEBUG_LOGGING
378 _logger.println(F("RNG failed."));
379#endif
380 session.clear();
381 CW_Utils::secure_wipe(sharedSecret, sizeof(sharedSecret));
382 CW_Utils::secure_wipe(sha512Output, sizeof(sha512Output));
383 CW_Utils::secure_wipe(concat, sizeof(concat));
384 return false;
385 }
386
387 /* Encrypt random data with Kenc (Bit padding) */
388 uint8_t ciphertextOPC[48U] = { 0U };
389 uint16_t cipherLength = _crypto.aesCbcEncrypt(RNG_data, sizeof(RNG_data),
390 ciphertextOPC,
391 session.aesKey, sizeof(session.aesKey),
392 iv_opc, true);
393
394 /* Compute MAC over APDU header + ciphertext (Null padding) */
395 uint8_t opcApduHeader[APDU_HEADER_LEN + APDU_LC_LEN] = {
396 0x80U, 0x11U, 0x00U, 0x00U,
397 (uint8_t)(cipherLength + AES_BLOCK_SIZE)
398 };
399 uint8_t MAC_apduHeader[AES_BLOCK_SIZE] = { 0U };
400 (void)CW_Utils::safe_memcpy(MAC_apduHeader, sizeof(MAC_apduHeader), opcApduHeader, sizeof(opcApduHeader));
401
402 size_t MAC_data_length = sizeof(MAC_apduHeader) + cipherLength;
403 uint8_t MAC_data[64U] = { 0U };
404 uint8_t ciphertextMACLong[64U] = { 0U };
405
406 if (MAC_data_length > sizeof(MAC_data)) {
407 session.clear();
408 CW_Utils::secure_wipe(sharedSecret, sizeof(sharedSecret));
409 CW_Utils::secure_wipe(sha512Output, sizeof(sha512Output));
410 CW_Utils::secure_wipe(concat, sizeof(concat));
411 CW_Utils::secure_wipe(RNG_data, sizeof(RNG_data));
412 return false;
413 }
414
415 (void)CW_Utils::safe_memcpy(MAC_data, sizeof(MAC_data), MAC_apduHeader, sizeof(MAC_apduHeader));
416 (void)CW_Utils::safe_memcpy(MAC_data + sizeof(MAC_apduHeader), sizeof(MAC_data) - sizeof(MAC_apduHeader), ciphertextOPC, cipherLength);
417
418 uint16_t encryptedLengthMAC = _crypto.aesCbcEncrypt(MAC_data, (uint16_t)MAC_data_length,
419 ciphertextMACLong,
420 session.macKey, sizeof(session.macKey),
421 mac_iv, false);
422
423 uint8_t MAC_value[AES_BLOCK_SIZE] = { 0U };
424 uint8_t macOffset = (uint8_t)(encryptedLengthMAC - AES_BLOCK_SIZE);
425 (void)CW_Utils::safe_memcpy(MAC_value, sizeof(MAC_value), ciphertextMACLong + macOffset, AES_BLOCK_SIZE);
426
427 /* Forge MUTUALLY AUTHENTICATE APDU */
428 uint8_t sendApduOpc[REQUEST_MUTUALLYAUTHENTICATE_IN_BYTES] = { 0U };
429 uint16_t offset = 0U;
430 (void)CW_Utils::safe_memcpy(sendApduOpc + offset, sizeof(sendApduOpc) - static_cast<size_t>(offset), opcApduHeader, sizeof(opcApduHeader));
431 offset += sizeof(opcApduHeader);
432 (void)CW_Utils::safe_memcpy(sendApduOpc + offset, sizeof(sendApduOpc) - static_cast<size_t>(offset), MAC_value, sizeof(MAC_value));
433 offset += sizeof(MAC_value);
434 (void)CW_Utils::safe_memcpy(sendApduOpc + offset, sizeof(sendApduOpc) - static_cast<size_t>(offset), ciphertextOPC, cipherLength);
435
436 uint8_t response[255U] = { 0U };
437 uint8_t responseLength = static_cast<uint8_t>(sizeof(response));
438
439 if (_driver.sendAPDU(sendApduOpc, sizeof(sendApduOpc), response, responseLength)) {
440 if (checkStatusWord(response, responseLength, 0x90U, 0x00U)) {
441 if (responseLength == static_cast<uint8_t>(RESPONSE_MUTUALLYAUTHENTICATE_IN_BYTES)) {
442 (void)CW_Utils::safe_memcpy(session.iv, CW_IV_SIZE, response, CW_IV_SIZE);
443 ret = true;
444 } else {
445#if CW_DEBUG_LOGGING
446 _logger.println(F("MutualAuth: unexpected response size."));
447#endif
448 }
449 } else {
450#if CW_DEBUG_LOGGING
451 _logger.println(F("MutualAuth: bad SW."));
452#endif
453 }
454 } else {
455#if CW_DEBUG_LOGGING
456 _logger.println(F("MutualAuth APDU failed."));
457#endif
458 }
459
460 /* Secure cleanup */
461 CW_Utils::secure_wipe(sharedSecret, sizeof(sharedSecret));
462 CW_Utils::secure_wipe(sha512Output, sizeof(sha512Output));
463 CW_Utils::secure_wipe(concat, sizeof(concat));
464 CW_Utils::secure_wipe(RNG_data, sizeof(RNG_data));
465 CW_Utils::secure_wipe(ciphertextOPC, sizeof(ciphertextOPC));
466 CW_Utils::secure_wipe(MAC_data, sizeof(MAC_data));
467
468 /* If the APDU exchange failed after session keys were written, clear
469 * them now to prevent a half-initialised session from being used (CRIT-04). */
470 if (!ret) {
471 session.clear();
472 }
473 }
474
475 return ret;
476}
477
502 const uint8_t apdu[], uint16_t apduLength,
503 const uint8_t data[], uint16_t dataLength,
504 uint8_t* decryptedOutput, uint16_t* decryptedOutputLength) {
505 bool ret = false;
506
507 /* Reject payloads that would overflow s_dataBuf (MED-01). */
508 if (dataLength > INPUT_BUFFER_LIMIT) {
509#if CW_DEBUG_LOGGING
510 _logger.println(F("Error: data too large for encryption buffer."));
511#endif
512 return false;
513 }
514
515 /* 1. Encrypt data with Kenc (Bit padding) */
516 uint16_t encryptedLength = _crypto.aesCbcEncrypt(data, dataLength, s_dataBuf,
517 session.aesKey, sizeof(session.aesKey),
518 session.iv, true);
519
520 uint16_t lcValue = encryptedLength + (uint16_t)AES_BLOCK_SIZE;
521 /* lcValue must fit in uint8_t: with INPUT_BUFFER_LIMIT=208, max encryptedLength=224,
522 * so max lcValue=240. The static_assert above also caps APDU at 255 bytes (MED-03). */
523 if (lcValue > 0xFFU) {
524#if CW_DEBUG_LOGGING
525 _logger.println(F("Error: lcValue overflow — payload too large."));
526#endif
528 return false;
529 }
530 uint8_t macApdu[MAC_APDU_LEN] = { 0U };
531 /* MED-03: Single-byte length encoding and no direction byte are intentional —
532 * this CBC-MAC construction matches the Cryptnox SCCP card firmware spec.
533 * Changing to AES-CMAC, wider encoding, or adding a direction byte would
534 * break the card protocol. lcValue overflow is prevented by the
535 * INPUT_BUFFER_LIMIT precondition above (dataLength <= 208 → lcValue <= 240). */
536 macApdu[0U] = (uint8_t)lcValue;
537
538 uint16_t macDataLength = apduLength + sizeof(macApdu) + encryptedLength;
539 if (macDataLength > MAX_MAC_DATA_LEN) {
540#if CW_DEBUG_LOGGING
541 _logger.println(F("Error: MAC data length exceeds buffer."));
542#endif
544 return false;
545 }
546
547 /* 2. Build MAC input: APDU header || LC block || ciphertext */
548 uint16_t offset = 0U;
549 (void)CW_Utils::safe_memcpy(s_macBuf, sizeof(s_macBuf), apdu, apduLength);
550 offset += apduLength;
551 (void)CW_Utils::safe_memcpy(s_macBuf + offset, sizeof(s_macBuf) - static_cast<size_t>(offset), macApdu, sizeof(macApdu));
552 offset += sizeof(macApdu);
553 (void)CW_Utils::safe_memcpy(s_macBuf + offset, sizeof(s_macBuf) - static_cast<size_t>(offset), s_dataBuf, encryptedLength);
554
555 uint8_t macIv[AES_BLOCK_SIZE] = { 0U };
556 uint16_t macEncryptedLength = _crypto.aesCbcEncrypt(s_macBuf, macDataLength, s_apduBuf,
557 session.macKey, sizeof(session.macKey),
558 macIv, false);
559
560 uint8_t macValue[AES_BLOCK_SIZE] = { 0U };
561 uint16_t macOffset = macEncryptedLength - AES_BLOCK_SIZE;
562 (void)CW_Utils::safe_memcpy(macValue, sizeof(macValue), s_apduBuf + macOffset, AES_BLOCK_SIZE);
563
564 /* 3. Build send APDU: header || Lc || MAC || ciphertext */
565 const uint8_t lc = (uint8_t)lcValue;
566 uint8_t sendApduLength = (uint8_t)(apduLength + APDU_LC_LEN + sizeof(macValue) + encryptedLength);
567 if (sendApduLength > SEND_APDU_MAX_LEN) {
568#if CW_DEBUG_LOGGING
569 _logger.println(F("Error: Send APDU length exceeds buffer."));
570#endif
573 return false;
574 }
575
576 offset = 0U;
577 (void)CW_Utils::safe_memcpy(s_apduBuf, sizeof(s_apduBuf), apdu, apduLength);
578 offset += apduLength;
579 s_apduBuf[offset] = lc;
580 offset += APDU_LC_LEN;
581 (void)CW_Utils::safe_memcpy(s_apduBuf + offset, sizeof(s_apduBuf) - static_cast<size_t>(offset), macValue, sizeof(macValue));
582 offset += sizeof(macValue);
583 (void)CW_Utils::safe_memcpy(s_apduBuf + offset, sizeof(s_apduBuf) - static_cast<size_t>(offset), s_dataBuf, encryptedLength);
584
585 /* 4. Send APDU */
586 uint8_t response[255U] = { 0U };
587 uint8_t responseLength = static_cast<uint8_t>(sizeof(response));
588
589 if (_driver.sendAPDU(s_apduBuf, sendApduLength, response, responseLength)) {
590 if (checkStatusWord(response, responseLength, 0x90U, 0x00U)) {
591 /* Update session.iv ONLY after the response MAC is verified (HIGH-01).
592 * Moving it before aesCbcDecrypt would let an attacker-chosen IV
593 * desynchronise the rolling-IV state even on MAC failure. */
594 ret = aesCbcDecrypt(session, response, static_cast<size_t>(responseLength), macValue,
595 decryptedOutput, decryptedOutputLength);
596 if (ret) {
597 (void)CW_Utils::safe_memcpy(session.iv, CW_IV_SIZE, response, CW_IV_SIZE);
598 } else {
599 session.clear();
600 }
601 } else if (responseLength >= 2U) {
602 /* Card-level error: surface the SW so the caller can diagnose
603 * common cases (e.g. 0x63Cn = wrong PIN with n retries left). */
604#if CW_DEBUG_LOGGING
605 _logger.print(F("Secured APDU: bad SW 0x"));
606 if (response[responseLength - 2U] < 0x10U) { _logger.print(F("0")); }
607 _logger.print(response[responseLength - 2U], HEX);
608 if (response[responseLength - 1U] < 0x10U) { _logger.print(F("0")); }
609 _logger.println(response[responseLength - 1U], HEX);
610#endif
611 }
612 } else {
613#if CW_DEBUG_LOGGING
614 _logger.println(F("Secured APDU failed."));
615#endif
616 }
617
618 /* Wipe plaintext scratch buffers so they do not persist in .bss (MED-04). */
621
622 return ret;
623}
624
626 uint8_t* response, size_t response_len,
627 uint8_t* mac_value,
628 uint8_t* decryptedOutput, uint16_t* decryptedOutputLength) {
629 /* Precondition: response must hold at least MAC(16) + 1 ciphertext byte + SW(2) (HIGH-02).
630 * Without this check, response_len < 18 causes size_t underflow in the subtractions below. */
631 if ((response == NULL) || (response_len < (size_t)(AES_BLOCK_SIZE + 2U + 1U))) {
632 return false;
633 }
634
635 /* Response layout: MAC(16) || cipherText(N) || SW1(1) || SW2(1) */
636 uint8_t rep_mac[AES_BLOCK_SIZE];
637 (void)CW_Utils::safe_memcpy(rep_mac, sizeof(rep_mac), response, AES_BLOCK_SIZE);
638 uint8_t* rep_data = response + AES_BLOCK_SIZE;
639 size_t totalDataLen = response_len - 2U;
640 size_t cipherLen = totalDataLen - AES_BLOCK_SIZE;
641
642 if (mac_value == NULL) {
643 return false;
644 }
645
646 /* Verify MAC: AES-CBC-MAC over [length_header(16)] || [all_ciphertext] */
647 size_t macInputLen = AES_BLOCK_SIZE + cipherLen;
648 if (macInputLen > sizeof(s_macBuf)) {
649#if CW_DEBUG_LOGGING
650 _logger.println(F("Error: Response too large for MAC verification."));
651#endif
652 return false;
653 }
654
655 memset(s_macBuf, 0U, AES_BLOCK_SIZE);
656 s_macBuf[0] = (uint8_t)totalDataLen;
657 (void)CW_Utils::safe_memcpy(s_macBuf + AES_BLOCK_SIZE, sizeof(s_macBuf) - AES_BLOCK_SIZE, rep_data, cipherLen);
658
659 uint8_t mac_iv[AES_BLOCK_SIZE] = { 0U };
660 uint16_t macEncryptedLength = _crypto.aesCbcEncrypt(s_macBuf, (uint16_t)macInputLen, s_apduBuf,
661 session.macKey, sizeof(session.macKey),
662 mac_iv, false);
663
664 uint8_t recomputedMacValue[AES_BLOCK_SIZE] = { 0U };
665 uint16_t macOffset = macEncryptedLength - AES_BLOCK_SIZE;
666 (void)CW_Utils::safe_memcpy(recomputedMacValue, sizeof(recomputedMacValue), s_apduBuf + macOffset, AES_BLOCK_SIZE);
667
668 if (!CW_Utils::secure_compare(rep_mac, recomputedMacValue, AES_BLOCK_SIZE)) {
669#if CW_DEBUG_LOGGING
670 _logger.println(F("MAC mismatch."));
671#endif
673 return false;
674 }
675
676 /* Decrypt ciphertext using mac_value as IV (Bit padding removal) */
677 uint16_t decryptedDataLength = _crypto.aesCbcDecrypt(rep_data, (uint16_t)cipherLen, s_dataBuf,
678 session.aesKey, sizeof(session.aesKey),
679 mac_value, true);
680
681 bool ret = false;
682
683 if (decryptedDataLength < 2U) {
684#if CW_DEBUG_LOGGING
685 _logger.println(F("Error: Decoded data too short."));
686#endif
687 }
688 else if (decryptedDataLength > sizeof(s_dataBuf)) {
689#if CW_DEBUG_LOGGING
690 _logger.println(F("Error: Decoded data length exceeds buffer."));
691#endif
692 }
693 else {
694 uint8_t innerSW1 = s_dataBuf[decryptedDataLength - 2U];
695 uint8_t innerSW2 = s_dataBuf[decryptedDataLength - 1U];
696 uint16_t payloadLength = decryptedDataLength - 2U;
697
698 if ((innerSW1 != 0x90U) || (innerSW2 != 0x00U)) {
699#if CW_DEBUG_LOGGING
700 _logger.print(F("Card error SW: 0x"));
701 if (innerSW1 < 0x10U) { _logger.print(F("0")); }
702 _logger.print(innerSW1, HEX);
703 _logger.print(F(" 0x"));
704 if (innerSW2 < 0x10U) { _logger.print(F("0")); }
705 _logger.println(innerSW2, HEX);
706#endif
707 }
708 else {
709 ret = true;
710 }
711
712 if ((decryptedOutput != NULL) && (decryptedOutputLength != NULL)) {
713 (void)CW_Utils::safe_memcpy(decryptedOutput, sizeof(s_dataBuf), s_dataBuf, payloadLength);
714 *decryptedOutputLength = payloadLength;
715 }
716 }
717
718 /* Wipe plaintext scratch buffers (MED-04). */
721
722 return ret;
723}
724
725/******************************************************************
726 * Certificate verification — static helpers
727 ******************************************************************/
728
729
730/* Read the DER length at buf[*pos]; advance *pos past the length bytes.
731 * Supports short form and long form with 1 or 2 extra bytes only. */
732static bool derReadLength(const uint8_t* buf, uint16_t bufLen,
733 uint16_t& pos, uint16_t& fieldLen) {
734 bool ok = false;
735 fieldLen = 0U;
736
737 if ((buf != NULL) && (pos < bufLen)) {
738 uint8_t b = buf[pos];
739 pos += 1U;
740
741 if ((b & DER_LEN_LONG_FLAG) == 0U) {
742 fieldLen = static_cast<uint16_t>(b);
743 ok = true;
744 } else if (b == DER_LEN_LONG_1) {
745 if (pos < bufLen) {
746 fieldLen = static_cast<uint16_t>(buf[pos]);
747 pos += 1U;
748 ok = true;
749 }
750 } else if (b == DER_LEN_LONG_2) {
751 if ((pos + 1U) < bufLen) {
752 fieldLen = static_cast<uint16_t>(
753 static_cast<uint16_t>(buf[pos]) << 8U);
754 fieldLen |= static_cast<uint16_t>(buf[pos + 1U]);
755 pos += 2U;
756 ok = true;
757 }
758 } else {
759 ok = false; /* indefinite form or > 2 extra bytes — unsupported */
760 }
761 }
762
763 return ok;
764}
765
766/* Skip one complete DER TLV (tag byte + length bytes + value) at buf[*pos]. */
767static bool derSkipField(const uint8_t* buf, uint16_t bufLen, uint16_t& pos) {
768 bool ok = false;
769
770 if ((buf != NULL) && (pos < bufLen)) {
771 uint16_t contentLen = 0U;
772 pos += 1U; /* skip tag byte */
773 if (derReadLength(buf, bufLen, pos, contentLen)) {
774 if ((pos + contentLen) <= bufLen) {
775 pos += contentLen;
776 ok = true;
777 }
778 }
779 }
780
781 return ok;
782}
783
784/* Walk a DER X.509 Certificate to extract — without any byte-pattern search:
785 * tbsMsgStart offset of TBSCertificate SEQUENCE tag inside buf
786 * tbsMsgLen total byte count of TBSCertificate (tag + length + content)
787 * pubKey65Ptr pointer to 65-byte uncompressed EC point (0x04 || X || Y)
788 * sigPtr pointer to the DER ECDSA signature bytes
789 * sigLen byte count of the DER ECDSA signature
790 * Returns false on any format or bounds error. */
791static bool derWalkMfCert(const uint8_t* buf, uint16_t bufLen,
792 uint16_t& tbsMsgStart, uint16_t& tbsMsgLen,
793 const uint8_t*& pubKey65Ptr,
794 const uint8_t*& sigPtr, uint8_t& sigLen) {
795 bool ok = true;
796 uint16_t pos = 0U;
797 uint16_t certContentLen = 0U;
798 uint16_t tbsContentLen = 0U;
799 uint16_t tbsEnd = 0U;
800 uint16_t spkiContentLen = 0U;
801 uint16_t bsLen = 0U;
802
803 tbsMsgStart = 0U;
804 tbsMsgLen = 0U;
805 pubKey65Ptr = NULL;
806 sigPtr = NULL;
807 sigLen = 0U;
808
809 if ((buf == NULL) || (bufLen == 0U)) {
810 ok = false;
811 }
812
813 /* ── outer Certificate SEQUENCE ── */
814 if (ok) {
815 if (buf[pos] != DER_TAG_SEQUENCE) {
816 ok = false;
817 }
818 }
819 if (ok) {
820 pos += 1U;
821 if (!derReadLength(buf, bufLen, pos, certContentLen)) {
822 ok = false;
823 }
824 }
825 if (ok) {
826 if ((pos + certContentLen) > bufLen) {
827 ok = false;
828 }
829 }
830
831 /* ── TBSCertificate SEQUENCE (first child) ── */
832 if (ok) {
833 if (buf[pos] != DER_TAG_SEQUENCE) {
834 ok = false;
835 }
836 }
837 if (ok) {
838 tbsMsgStart = pos;
839 pos += 1U;
840 if (!derReadLength(buf, bufLen, pos, tbsContentLen)) {
841 ok = false;
842 }
843 }
844 if (ok) {
845 tbsMsgLen = (pos - tbsMsgStart) + tbsContentLen;
846 tbsEnd = pos + tbsContentLen;
847 if (tbsEnd > bufLen) {
848 ok = false;
849 }
850 }
851
852 /* ── Walk TBSCertificate fields in order ── */
853
854 /* [0] EXPLICIT version — present in X.509 v3 */
855 if (ok && (pos < tbsEnd)) {
856 if (buf[pos] == DER_TAG_CTX0) {
857 if (!derSkipField(buf, tbsEnd, pos)) {
858 ok = false;
859 }
860 }
861 }
862
863 /* serialNumber INTEGER */
864 if (ok) {
865 if (!derSkipField(buf, tbsEnd, pos)) {
866 ok = false;
867 }
868 }
869
870 /* signature AlgorithmIdentifier SEQUENCE */
871 if (ok) {
872 if (!derSkipField(buf, tbsEnd, pos)) {
873 ok = false;
874 }
875 }
876
877 /* issuer Name SEQUENCE */
878 if (ok) {
879 if (!derSkipField(buf, tbsEnd, pos)) {
880 ok = false;
881 }
882 }
883
884 /* validity SEQUENCE */
885 if (ok) {
886 if (!derSkipField(buf, tbsEnd, pos)) {
887 ok = false;
888 }
889 }
890
891 /* subject Name SEQUENCE */
892 if (ok) {
893 if (!derSkipField(buf, tbsEnd, pos)) {
894 ok = false;
895 }
896 }
897
898 /* ── SubjectPublicKeyInfo SEQUENCE ── */
899 if (ok) {
900 if (pos >= tbsEnd) {
901 ok = false;
902 }
903 }
904 if (ok) {
905 if (buf[pos] != DER_TAG_SEQUENCE) {
906 ok = false;
907 }
908 }
909 if (ok) {
910 pos += 1U;
911 if (!derReadLength(buf, bufLen, pos, spkiContentLen)) {
912 ok = false;
913 }
914 }
915 if (ok) {
916 if ((pos + spkiContentLen) > bufLen) {
917 ok = false;
918 }
919 }
920
921 /* Skip AlgorithmIdentifier SEQUENCE inside SubjectPublicKeyInfo */
922 if (ok) {
923 if (!derSkipField(buf, bufLen, pos)) {
924 ok = false;
925 }
926 }
927
928 /* subjectPublicKey BIT STRING */
929 if (ok) {
930 if (pos >= bufLen) {
931 ok = false;
932 }
933 }
934 if (ok) {
935 if (buf[pos] != DER_TAG_BIT_STRING) {
936 ok = false;
937 }
938 }
939 if (ok) {
940 pos += 1U;
941 if (!derReadLength(buf, bufLen, pos, bsLen)) {
942 ok = false;
943 }
944 }
945 if (ok) {
946 if ((pos + bsLen) > bufLen) {
947 ok = false;
948 }
949 }
950 if (ok) {
951 if (bsLen < (1U + static_cast<uint16_t>(DER_EC_POINT_BYTES))) {
952 ok = false; /* too short: unused-bits byte + 65-byte EC point */
953 }
954 }
955 if (ok) {
956 if (buf[pos] != DER_BIT_UNUSED_ZERO) {
957 ok = false; /* unused bits must be 0 */
958 } else if (buf[pos + 1U] != DER_EC_UNCOMPRESSED) {
959 ok = false; /* must be an uncompressed EC point */
960 } else {
961 pubKey65Ptr = buf + pos + 1U; /* points to: 0x04 || X[32] || Y[32] */
962 }
963 }
964
965 /* Jump to end of TBSCertificate, then skip signatureAlgorithm SEQUENCE */
966 if (ok) {
967 pos = tbsEnd;
968 if (!derSkipField(buf, bufLen, pos)) {
969 ok = false;
970 }
971 }
972
973 /* ── signatureValue BIT STRING (third child of Certificate) ── */
974 if (ok) {
975 if (pos >= bufLen) {
976 ok = false;
977 }
978 }
979 if (ok) {
980 if (buf[pos] != DER_TAG_BIT_STRING) {
981 ok = false;
982 }
983 }
984 if (ok) {
985 pos += 1U;
986 if (!derReadLength(buf, bufLen, pos, bsLen)) {
987 ok = false;
988 }
989 }
990 if (ok) {
991 if ((pos + bsLen) > bufLen) {
992 ok = false;
993 }
994 }
995 if (ok) {
996 if (bsLen < 2U) {
997 ok = false; /* need unused-bits byte + at least 1 signature byte */
998 }
999 }
1000 if (ok) {
1001 if (buf[pos] != DER_BIT_UNUSED_ZERO) {
1002 ok = false; /* unused bits must be 0 */
1003 }
1004 }
1005 if (ok) {
1006 uint16_t rawSigLen = bsLen - 1U;
1007 if (rawSigLen > 255U) {
1008 ok = false;
1009 } else {
1010 sigPtr = buf + pos + 1U; /* DER ECDSA signature, after unused-bits byte */
1011 sigLen = static_cast<uint8_t>(rawSigLen);
1012 }
1013 }
1014
1015 return ok;
1016}
1017
1018bool CW_SecureChannel::parseDerSigToRaw(const uint8_t* der, uint8_t derLen,
1019 uint8_t* raw64) {
1020 bool ret = false;
1021
1022 if ((der != NULL) && (raw64 != NULL) && (derLen >= 6U) && (der[0] == 0x30U)) {
1023 /* Validate outer SEQUENCE length against actual buffer (HIGH-04). */
1024 if ((uint8_t)(der[1] + 2U) > derLen) {
1025 return false;
1026 }
1027
1028 uint8_t pos = 2U; /* skip SEQUENCE tag + length */
1029
1030 if (der[pos] == 0x02U) {
1031 pos++;
1032 uint8_t rLen = der[pos];
1033 pos++;
1034 /* Reject malformed r — DER r is at most 33 bytes (32 + 1 zero pad) (HIGH-04). */
1035 if (rLen > 33U) {
1036 return false;
1037 }
1038 if ((pos + rLen) <= derLen) {
1039 const uint8_t* rPtr = der + pos;
1040 pos += rLen;
1041
1042 if ((pos < derLen) && (der[pos] == 0x02U)) {
1043 pos++;
1044 uint8_t sLen = der[pos];
1045 pos++;
1046 /* Reject malformed s (HIGH-04). */
1047 if (sLen > 33U) {
1048 return false;
1049 }
1050 if ((pos + sLen) <= derLen) {
1051 /* M-06: verify sum of both INTEGER fields matches the outer SEQUENCE length.
1052 * Trailing garbage after s would indicate malformed/crafted DER. */
1053 if ((pos + sLen) != (uint8_t)(2U + der[1])) {
1054 return false;
1055 }
1056 const uint8_t* sPtr = der + pos;
1057
1058 memset(raw64, 0U, 64U);
1059
1060 /* r: strip optional leading 0x00 padding byte */
1061 if ((rLen == 33U) && (rPtr[0] == 0x00U)) { rPtr++; rLen = 32U; }
1062 if (rLen <= 32U) {
1063 (void)CW_Utils::safe_memcpy(raw64 + (32U - rLen), static_cast<size_t>(32U + rLen), rPtr, rLen);
1064 }
1065
1066 /* s: strip optional leading 0x00 padding byte */
1067 if ((sLen == 33U) && (sPtr[0] == 0x00U)) { sPtr++; sLen = 32U; }
1068 if (sLen <= 32U) {
1069 (void)CW_Utils::safe_memcpy(raw64 + 32U + (32U - sLen), static_cast<size_t>(sLen), sPtr, sLen);
1070 }
1071
1072 ret = true;
1073 }
1074 }
1075 }
1076 }
1077 }
1078
1079 return ret;
1080}
1081
1082bool CW_SecureChannel::verifyEcdsaSha256(const uint8_t* pubKey64,
1083 const uint8_t* message, uint16_t msgLen,
1084 const uint8_t* derSig, uint8_t derSigLen) {
1085 bool result = false;
1086 uint8_t hash[32U] = { 0U };
1087 uint8_t rawSig[64U] = { 0U };
1088
1089 bool hashOk = _crypto.sha256(message, msgLen, hash);
1090
1091 if (hashOk && parseDerSigToRaw(derSig, derSigLen, rawSig)) {
1092 result = _crypto.ecdsaVerify(pubKey64, hash, sizeof(hash), rawSig, CW_CURVE_SECP256R1);
1093 }
1094
1095 return result;
1096}
1097
1098bool CW_SecureChannel::getManufacturerCertificate(uint8_t* cert, uint16_t& certLen) {
1099 bool ret = false;
1100 certLen = 0U;
1101
1102 if (cert != NULL) {
1103 const uint8_t APDU_P2_IDX = 3U; /* P2 field offset in ISO 7816-4 APDU header */
1104 uint8_t apdu[5U] = { 0x80U, 0xF7U, 0x00U, 0x00U, 0x00U };
1106 uint16_t responseLen = static_cast<uint16_t>(sizeof(response));
1107
1108 if (!_driver.sendAPDULarge(apdu, static_cast<uint8_t>(sizeof(apdu)), response,
1109 responseLen)) {
1110#if CW_DEBUG_LOGGING
1111 _logger.println(F("getManufacturerCertificate APDU failed."));
1112#endif
1113 return false;
1114 }
1115 if (responseLen > static_cast<uint16_t>(sizeof(response))) {
1116#if CW_DEBUG_LOGGING
1117 _logger.println(F("getManufacturerCertificate: driver reported overflow."));
1118#endif
1119 return false;
1120 }
1121
1122 if (checkStatusWord(response, responseLen, 0x90U, 0x00U)) {
1123
1124 uint16_t dataBytes = static_cast<uint16_t>(
1125 responseLen - static_cast<uint16_t>(RESPONSE_STATUS_WORDS_IN_BYTES));
1126
1127 if (dataBytes >= 2U) {
1128 uint16_t totalCertLen = (static_cast<uint16_t>(response[0]) << 8U)
1129 | static_cast<uint16_t>(response[1]);
1130
1131 if (totalCertLen <= CW_MANUF_CERT_MAX_BYTES) {
1132 uint16_t certInPage = static_cast<uint16_t>(dataBytes - 2U);
1133 if (certInPage > totalCertLen) {
1134 certInPage = totalCertLen;
1135 }
1136 (void)CW_Utils::safe_memcpy(cert, CW_MANUF_CERT_MAX_BYTES, response + 2U,
1137 static_cast<size_t>(certInPage));
1138 certLen = certInPage;
1139
1140 uint8_t pageIdx = 1U;
1141 while ((certLen < totalCertLen) && (pageIdx < 8U)) {
1142 apdu[APDU_P2_IDX] = pageIdx;
1143 responseLen = static_cast<uint16_t>(sizeof(response));
1144
1145 if (!_driver.sendAPDULarge(apdu, static_cast<uint8_t>(sizeof(apdu)),
1146 response, responseLen)) {
1147 break;
1148 }
1149 if (responseLen > static_cast<uint16_t>(sizeof(response))) {
1150#if CW_DEBUG_LOGGING
1151 _logger.println(F("getManufacturerCertificate: driver reported overflow."));
1152#endif
1153 return false;
1154 }
1155 if (!checkStatusWord(response, responseLen, 0x90U, 0x00U)) {
1156 break;
1157 }
1158
1159 uint16_t pageData = static_cast<uint16_t>(
1160 responseLen - static_cast<uint16_t>(RESPONSE_STATUS_WORDS_IN_BYTES));
1161 uint16_t remaining = static_cast<uint16_t>(totalCertLen - certLen);
1162 if (pageData > remaining) {
1163 pageData = remaining;
1164 }
1165
1166 if ((certLen + pageData) > static_cast<uint16_t>(CW_MANUF_CERT_MAX_BYTES)) {
1167 break;
1168 }
1169 (void)CW_Utils::safe_memcpy(cert + certLen,
1170 CW_MANUF_CERT_MAX_BYTES - static_cast<size_t>(certLen),
1171 response, static_cast<size_t>(pageData));
1172 certLen = static_cast<uint16_t>(certLen + pageData);
1173 pageIdx++;
1174 }
1175
1176 ret = (certLen == totalCertLen);
1177 if (!ret) {
1178#if CW_DEBUG_LOGGING
1179 _logger.println(F("getManufacturerCertificate: incomplete."));
1180#endif
1181 }
1182 } else {
1183#if CW_DEBUG_LOGGING
1184 _logger.println(F("getManufacturerCertificate: cert too large."));
1185#endif
1186 }
1187 }
1188 }
1189 }
1190
1191 return ret;
1192}
1193
1195 _cachedMfCertLen = 0U;
1197 if (!result) {
1198 _cachedMfCertLen = 0U;
1199 }
1200 return result;
1201}
1202
1227uint8_t CW_SecureChannel::verifyCertificateChain(const uint8_t* cardCert,
1228 uint8_t cardCertLen) {
1229 uint8_t result = CW_CERT_OK;
1230
1231 if ((cardCert == NULL) || (cardCertLen < 80U) || (cardCert[0] != 0x43U)) {
1232#if CW_DEBUG_LOGGING
1233 _logger.println(F("verifyCert: invalid card cert."));
1234#endif
1235 result = CW_CERT_FORMAT_ERROR;
1236 }
1237
1238 /* Consume the pre-fetched cert (filled by preFetchManufacturerCert() before
1239 * getCardCertificate() was called). Fall back to fetching now if none cached —
1240 * this will fail on cards whose state machine has already advanced past INS=F7. */
1241 uint16_t mfCertLen = _cachedMfCertLen;
1242 _cachedMfCertLen = 0U;
1243
1244 if (result == CW_CERT_OK) {
1245 if (mfCertLen == 0U) {
1246 if (!getManufacturerCertificate(s_mfCertBuf, mfCertLen) || (mfCertLen < 20U)) {
1247#if CW_DEBUG_LOGGING
1248 _logger.println(F("verifyCert: failed to get mfr cert."));
1249#endif
1250 result = CW_CERT_FORMAT_ERROR;
1251 }
1252 } else if (mfCertLen < 20U) {
1253#if CW_DEBUG_LOGGING
1254 _logger.println(F("verifyCert: pre-fetched mfr cert too short."));
1255#endif
1256 result = CW_CERT_FORMAT_ERROR;
1257 } else {
1258 /* Pre-fetched cert in s_mfCertBuf is valid — no APDU needed. */
1259 }
1260 }
1261
1262 /* MED-05: replace byte-pattern search with a structural DER walker that
1263 * enforces field order. This prevents attacker-planted OID sequences in
1264 * unsigned extensions from being picked up as the device public key. */
1265 uint16_t tbsMsgStart = 0U;
1266 uint16_t tbsMsgLen = 0U;
1267 const uint8_t* pubKey65Ptr = NULL;
1268 const uint8_t* mfSigPtr = NULL;
1269 uint8_t mfSigLen = 0U;
1270
1271 if (result == CW_CERT_OK) {
1272 if (!derWalkMfCert(s_mfCertBuf, mfCertLen,
1273 tbsMsgStart, tbsMsgLen,
1274 pubKey65Ptr,
1275 mfSigPtr, mfSigLen)) {
1276#if CW_DEBUG_LOGGING
1277 _logger.println(F("verifyCert: DER walk of mfr cert failed."));
1278#endif
1279 result = CW_CERT_KEY_NOT_FOUND;
1280 }
1281 }
1282
1283 const uint8_t* devicePubKey64 = NULL;
1284 if (result == CW_CERT_OK) {
1285 devicePubKey64 = pubKey65Ptr + 1U; /* skip the 0x04 uncompressed prefix */
1286
1287 const uint8_t* mfMsg = s_mfCertBuf + tbsMsgStart;
1288 uint16_t mfMsgLen = tbsMsgLen;
1289
1290 bool mfVerified = false;
1291 for (uint8_t i = 0U; i < CW_TRUSTED_CA_COUNT; i++) {
1293 mfMsg, mfMsgLen,
1294 mfSigPtr, mfSigLen)) {
1295 mfVerified = true;
1296 break;
1297 }
1298 }
1299 if (!mfVerified) {
1300#if CW_DEBUG_LOGGING
1301 _logger.println(F("verifyCert: mfr cert sig INVALID — card NOT genuine."));
1302#endif
1304 }
1305#if CW_DEBUG_LOGGING
1306 else {
1307 _logger.println(F("Manufacturer cert signature OK."));
1308 }
1309#endif
1310 }
1311
1312 const uint8_t CARD_CERT_MSG_LEN = 74U;
1313 if (result == CW_CERT_OK) {
1314 if (cardCertLen <= CARD_CERT_MSG_LEN) {
1315#if CW_DEBUG_LOGGING
1316 _logger.println(F("verifyCert: card cert too short for sig."));
1317#endif
1318 result = CW_CERT_FORMAT_ERROR;
1319 }
1320 else {
1321 const uint8_t* cardSig = cardCert + CARD_CERT_MSG_LEN;
1322 uint8_t cardSigLen = cardCertLen - CARD_CERT_MSG_LEN;
1323
1324 if (!verifyEcdsaSha256(devicePubKey64,
1325 cardCert, CARD_CERT_MSG_LEN,
1326 cardSig, cardSigLen)) {
1327#if CW_DEBUG_LOGGING
1328 _logger.println(F("verifyCert: card cert sig INVALID."));
1329#endif
1330 result = CW_CERT_CARD_SIG_INVALID;
1331 }
1332#if CW_DEBUG_LOGGING
1333 else {
1334 _logger.println(F("Card cert signature OK."));
1335 }
1336#endif
1337 }
1338 }
1339
1340 if (result == CW_CERT_OK) {
1341 if ((cardCertLen <= (1U + CW_CERT_NONCE_SIZE)) ||
1342 (!CW_Utils::secure_compare(cardCert + 1U, _lastNonce, CW_CERT_NONCE_SIZE))) { /* M-03: constant-time nonce compare */
1343#if CW_DEBUG_LOGGING
1344 _logger.println(F("verifyCert: nonce mismatch — possible replay."));
1345#endif
1346 result = CW_CERT_NONCE_MISMATCH;
1347 }
1348 }
1349
1350#if CW_DEBUG_LOGGING
1351 if (result == CW_CERT_OK) {
1352 _logger.println(F("Certificate chain OK. Card is genuine."));
1353 }
1354#endif
1355
1356 /* Wipe manufacturer cert buffer — it held the card device public key (M-01). */
1358
1359 return result;
1360}
ArduinoPlatform platform
#define CW_IV_SIZE
Definition CW_Defs.h:77
#define CW_CERT_MANUF_SIG_INVALID
Definition CW_Defs.h:131
#define CW_CERT_FORMAT_ERROR
Definition CW_Defs.h:128
#define CW_CERT_KEY_NOT_FOUND
Definition CW_Defs.h:132
#define CW_MACKEY_SIZE
Definition CW_Defs.h:76
#define CW_CERT_NONCE_SIZE
Definition CW_Defs.h:124
#define CW_CERT_OK
Definition CW_Defs.h:127
#define CW_AESKEY_SIZE
Definition CW_Defs.h:75
#define CW_MANUF_CERT_MAX_BYTES
Definition CW_Defs.h:136
#define CW_CERT_CARD_SIG_INVALID
Definition CW_Defs.h:130
#define CW_CERT_NONCE_MISMATCH
Definition CW_Defs.h:129
@ CW_CURVE_SECP256R1
Definition CW_Defs.h:152
#define RESPONSE_STATUS_WORDS_IN_BYTES
static bool derSkipField(const uint8_t *buf, uint16_t bufLen, uint16_t &pos)
static bool derWalkMfCert(const uint8_t *buf, uint16_t bufLen, uint16_t &tbsMsgStart, uint16_t &tbsMsgLen, const uint8_t *&pubKey65Ptr, const uint8_t *&sigPtr, uint8_t &sigLen)
#define MAC_APDU_LEN
#define DER_TAG_SEQUENCE
#define DER_EC_POINT_BYTES
#define RESPONSE_GETCARDCERTIFICATE_IN_BYTES
#define DER_TAG_CTX0
#define RESPONSE_SELECT_IN_BYTES
#define DER_LEN_LONG_2
#define OPENSECURECHANNEL_SALT_IN_BYTES
#define REQUEST_MUTUALLYAUTHENTICATE_IN_BYTES
#define COMMON_PAIRING_DATA
#define DER_LEN_LONG_1
#define APDU_HEADER_LEN
#define APDU_LC_LEN
#define RESPONSE_OPENSECURECHANNEL_IN_BYTES
#define DER_BIT_UNUSED_ZERO
#define MAX_MAC_DATA_LEN
#define INPUT_BUFFER_LIMIT
#define RESPONSE_MUTUALLYAUTHENTICATE_IN_BYTES
#define RESPONSE_GETMANUFACTURERCERT_PAGE_IN_BYTES
static uint8_t s_mfCertBuf[CW_MANUF_CERT_MAX_BYTES]
static uint8_t s_dataBuf[ENC_BUF_MAX_LEN]
static uint8_t s_macBuf[MAX_MAC_DATA_LEN]
#define DER_EC_UNCOMPRESSED
static uint8_t s_apduBuf[SEND_APDU_MAX_LEN]
#define DER_TAG_BIT_STRING
#define SEND_APDU_MAX_LEN
static bool derReadLength(const uint8_t *buf, uint16_t bufLen, uint16_t &pos, uint16_t &fieldLen)
#define CLIENT_PUBLIC_KEY_SIZE
#define GETCARDCERTIFICATE_IN_BYTES
#define ENC_BUF_MAX_LEN
#define AES_BLOCK_SIZE
#define RANDOM_BYTES
#define DER_LEN_LONG_FLAG
Cryptnox secure channel protocol over NFC.
Cryptnox CA public keys used for offline certificate verification.
static const uint8_t *const CW_TRUSTED_CA_KEYS[CW_TRUSTED_CA_COUNT]
#define CW_TRUSTED_CA_COUNT
Platform-independent security and memory utilities.
Abstract interface for cryptographic operations used by CW_SecureChannel.
Abstract interface for serial/debug output.
Definition CW_Logger.h:48
Abstract interface for NFC transport operations.
Abstract interface for platform-specific operations used by the SDK.
Definition CW_Platform.h:39
CW_SecureChannel(CW_NfcTransport &driver, CW_Logger &logger, CW_CryptoProvider &crypto, CW_Platform &platform)
Construct a CW_SecureChannel.
static bool parseDerSigToRaw(const uint8_t *der, uint8_t derLen, uint8_t *raw64)
CW_Logger & _logger
Logging interface.
bool openSecureChannel(uint8_t *salt, uint8_t *clientPublicKey, uint8_t *clientPrivateKey, CW_Curve sessionCurve)
Send OPEN SECURE CHANNEL and retrieve the session salt.
CW_NfcTransport & _driver
NFC transport for APDU exchange.
bool mutuallyAuthenticate(CW_SecureSession &session, const uint8_t *salt, uint8_t *clientPublicKey, const uint8_t *clientPrivateKey, CW_Curve sessionCurve, const uint8_t *cardEphemeralPubKey)
Perform ECDH derivation and MUTUALLY AUTHENTICATE with the card.
bool aesCbcEncrypt(CW_SecureSession &session, const uint8_t apdu[], uint16_t apduLength, const uint8_t data[], uint16_t dataLength, uint8_t *decryptedOutput=NULL, uint16_t *decryptedOutputLength=NULL)
AES-CBC encrypt + MAC, send APDU, and decrypt response.
bool inListPassiveTarget()
Detect a passive NFC target (ISO-DEP card).
bool aesCbcDecrypt(const CW_SecureSession &session, uint8_t *response, size_t responseLen, uint8_t *macValue, uint8_t *decryptedOutput=NULL, uint16_t *decryptedOutputLength=NULL)
Verify MAC and decrypt an encrypted APDU response.
bool begin()
Initialize the NFC transport module.
bool verifyEcdsaSha256(const uint8_t *pubKey64, const uint8_t *message, uint16_t msgLen, const uint8_t *derSig, uint8_t derSigLen)
bool checkStatusWord(const uint8_t *response, uint16_t responseLength, uint8_t sw1Expected, uint8_t sw2Expected)
Verify the SW1/SW2 status word at the end of an APDU response.
bool preFetchManufacturerCert()
Fetch and cache the manufacturer certificate before getCardCertificate().
bool getCardCertificate(uint8_t *cardCertificate, uint8_t &cardCertificateLength)
Retrieve the card's ephemeral public key via GET CARD CERTIFICATE.
uint16_t _cachedMfCertLen
Non-zero when s_mfCertBuf holds a valid pre-fetched manufacturer certificate.
bool selectApdu()
Send the SELECT APDU to activate the Cryptnox application.
CW_Platform & _platform
Platform abstraction (sleep_ms).
uint8_t verifyCertificateChain(const uint8_t *cardCert, uint8_t cardCertLen)
Verify the full card certificate chain against the trusted CA.
void resetReader()
Reset the NFC reader hardware.
uint8_t _lastNonce[CW_CERT_NONCE_SIZE]
Nonce sent in the last getCardCertificate() call; checked in verifyCertificateChain().
bool extractCardEphemeralKey(const uint8_t *cardCertificate, uint8_t *cardEphemeralPubKey, uint8_t *fullEphemeralPubKey65=NULL)
Extract the card's ephemeral EC P-256 public key from a certificate.
CW_CryptoProvider & _crypto
Crypto operations (AES, SHA, ECDH, RNG).
bool getManufacturerCertificate(uint8_t *cert, uint16_t &certLen)
Retrieve the manufacturer certificate stored in card flash.
bool printFirmwareVersion()
Print the NFC reader firmware version to the logger.
static bool safe_memcpy(uint8_t *dst, size_t dstSize, const uint8_t *src, size_t count)
Safe memcpy — validates pointers, sizes, and checks for overlap.
Definition CW_Utils.cpp:50
static bool secure_compare(const uint8_t *a, const uint8_t *b, size_t len)
Constant-time buffer comparison, resistant to timing side-channel attacks.
Definition CW_Utils.cpp:22
static void secure_wipe(uint8_t *buf, size_t len)
Securely zero a buffer, guaranteed not to be optimised away.
Definition CW_Utils.cpp:37
CW_Curve
Portable curve identifier used throughout the SDK.
Definition CW_Defs.h:151
#define F(string_literal)
#define HEX
Holds cryptographic session state for reentrant secure channel operations.
Definition CW_Defs.h:168
uint8_t macKey[CW_MACKEY_SIZE]
Definition CW_Defs.h:170
void clear()
Securely clear all session keys and IV.
Definition CW_Defs.h:181
uint8_t aesKey[CW_AESKEY_SIZE]
Definition CW_Defs.h:169
uint8_t iv[CW_IV_SIZE]
Definition CW_Defs.h:171