feat(core/prodtest): Add pin padding to AUTHENTICATE and SET_PIN command.
What changed, and why it matters
This commit changes a Trezor factory-testing tool (prodtest) so that PINs sent during NFC backup authentication and PIN setup are padded to a fixed length and encrypted with the Noise XXpsk3 protocol before being transmitted. Previously, the raw PIN was placed directly into the NFC APDU command. The change reduces the risk that someone sniffing NFC traffic could learn the PIN length or the PIN itself in plaintext. It is a defensive hardening improvement in a manufacturing/debug utility, not a fix for an active exploit in end-user firmware.
Treat as a defensive hardening commit. Review the corresponding NFC backup applet/firmware receiver to ensure it expects and can decrypt the new padded, encrypted PIN format; verify that 0xFF padding is stripped correctly and that a PIN containing trailing 0xFF bytes cannot be confused with padding. Confirm the change is covered by prodtest integration tests before deployment to manufacturing.
Security signals we found
Plaintext PIN removed from NFC APDU data
Fixed-length PIN padding hides actual PIN length from passive observers
Noise XXpsk3 authenticated encryption applied to PIN commands
Input length bound added (NFC_BACKUP_MAX_PIN_LEN)
Magic AEAD tag size replaced with named constant ENC_TAG_SIZE
Evidence from the diff
In core/embed/projects/prodtest/cmd/prodtest_nfc_backup.c, the AUTHENTICATE (0x03) and SET_PIN (0x04) APDU builders now: (1) enforce pin_len <= 32, (2) pad the PIN with 0xFF to exactly 32 bytes, and (3) encrypt the padded PIN with noise_xxpsk3_send_message before calling nfc_backup_compose_apdu. The earlier code sent the raw PIN bytes as APDU data. A symbolic constant ENC_TAG_SIZE (16) replaces a magic number for the AEAD tag size in seed read/write. The change is consistent with the existing seed encryption path and removes a plaintext PIN leak in the prodtest NFC backup protocol.
Changed components
core/embed/projects/prodtest/cmd/prodtest_nfc_backup.cprodtest NFC backup AUTHENTICATE commandprodtest NFC backup SET_PIN commandInspect captured patch +43 / −7
### core/embed/projects/prodtest/cmd/prodtest_nfc_backup.c
@@ -36,6 +36,8 @@
#define NFC_BACKUP_SEED_METADATA_SIZE 256
#define NFC_BACKUP_SEED_SIZE 256
#define NFC_BACKUP_VERBOSE_SECRETS false
+#define NFC_BACKUP_MAX_PIN_LEN 32
+#define ENC_TAG_SIZE 16
static noise_xxpsk3_initiator_t intr = {0};
@@ -773,8 +775,25 @@ static ts_t api_authenticate(cli_t *cli, const char *pin, size_t pin_len) {
nfc_apdu_message_t cmd = {0};
nfc_apdu_message_t rsp = {0};
- status = nfc_backup_compose_apdu(0x80, 0x03, 0x00, 0x00, (const uint8_t *)pin,
- pin_len, &cmd);
+ TSH_CHECK_ARG(pin_len <= NFC_BACKUP_MAX_PIN_LEN);
+
+ // Pad pin with 0xFF
+ char pin_padded[NFC_BACKUP_MAX_PIN_LEN] = {0};
+ memcpy(pin_padded, pin, pin_len);
+ for (size_t i = pin_len; i < NFC_BACKUP_MAX_PIN_LEN; i++) {
+ pin_padded[i] = 0xFFU;
+ }
+
+ // Encrypt the PIN using Noise XXpsk3
+ uint8_t enc_pin[NFC_BACKUP_MAX_PIN_LEN + ENC_TAG_SIZE] = {0};
+ size_t enc_pin_size = 0;
+ bool ok = noise_xxpsk3_send_message(
+ &intr.transport_state, (const uint8_t *)pin_padded,
+ NFC_BACKUP_MAX_PIN_LEN, enc_pin, sizeof(enc_pin), &enc_pin_size);
+ TSH_CHECK(ok, TS_EINVAL);
+
+ status = nfc_backup_compose_apdu(
+ 0x80, 0x03, 0x00, 0x00, (const uint8_t *)enc_pin, enc_pin_size, &cmd);
TSH_CHECK_OK(status);
status = nfc_backup_transceive_logged(cli, "authenticate", 0x03, &cmd, &rsp);
@@ -796,8 +815,25 @@ static ts_t api_set_pin(cli_t *cli, const char *new_pin, size_t new_pin_len) {
nfc_apdu_message_t cmd = {0};
nfc_apdu_message_t rsp = {0};
- status = nfc_backup_compose_apdu(0x80, 0x04, 0x00, 0x00,
- (const uint8_t *)new_pin, new_pin_len, &cmd);
+ TSH_CHECK_ARG(new_pin_len <= NFC_BACKUP_MAX_PIN_LEN);
+
+ // Pad pin with 0xFF
+ uint8_t pin_padded[NFC_BACKUP_MAX_PIN_LEN] = {0};
+ memcpy(pin_padded, new_pin, new_pin_len);
+ for (size_t i = new_pin_len; i < NFC_BACKUP_MAX_PIN_LEN; i++) {
+ pin_padded[i] = 0xFFU;
+ }
+
+ // Encrypt the new PIN using Noise XXpsk3
+ uint8_t enc_pin[NFC_BACKUP_MAX_PIN_LEN + ENC_TAG_SIZE] = {0};
+ size_t enc_pin_size = 0;
+ bool ok = noise_xxpsk3_send_message(
+ &intr.transport_state, (const uint8_t *)pin_padded,
+ NFC_BACKUP_MAX_PIN_LEN, enc_pin, sizeof(enc_pin), &enc_pin_size);
+ TSH_CHECK(ok, TS_EINVAL);
+
+ status = nfc_backup_compose_apdu(
+ 0x80, 0x04, 0x00, 0x00, (const uint8_t *)enc_pin, enc_pin_size, &cmd);
TSH_CHECK_OK(status);
status = nfc_backup_transceive_logged(cli, "set-pin", 0x04, &cmd, &rsp);
@@ -1046,8 +1082,8 @@ static ts_t api_read_seed(cli_t *cli, uint8_t *seed, size_t seed_buf_size,
TSH_CHECK(seed_buf_size >= NFC_BACKUP_SEED_SIZE, TS_EINVAL);
bool ok = noise_xxpsk3_receive_message(&intr.transport_state, rsp.data,
- NFC_BACKUP_SEED_SIZE + 16, seed,
- seed_buf_size, seed_len);
+ NFC_BACKUP_SEED_SIZE + ENC_TAG_SIZE,
+ seed, seed_buf_size, seed_len);
TSH_CHECK(ok, TS_EINVAL);
cleanup:
@@ -1064,7 +1100,7 @@ static ts_t api_write_seed(cli_t *cli, const uint8_t *seed, size_t seed_len) {
TSH_CHECK_ARG(seed != NULL);
TSH_CHECK_ARG(seed_len == NFC_BACKUP_SEED_SIZE);
- uint8_t enc_seed[NFC_BACKUP_SEED_SIZE + 16] = {0};
+ uint8_t enc_seed[NFC_BACKUP_SEED_SIZE + ENC_TAG_SIZE] = {0};
size_t enc_seed_size = 0;
bool ok =
noise_xxpsk3_send_message(&intr.transport_state, seed, seed_len, enc_seed,Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.