feat(core/prodtest): introduce CRC checks for CLI commands
What changed, and why it matters
This commit adds an optional CRC-32 checksum feature to the Trezor production-test command-line interface (prodtest CLI). It is a data-integrity feature, not a fix for an existing security vulnerability. The change lets factory-test tools request that every command and response include a CRC checksum, helping detect accidental communication corruption. There is no indication in the commit that this addresses a known attack or security flaw.
Treat as a routine feature addition. Reviewers may verify that CRC parsing cannot be confused by short lines (the `len >= 9` guard is present) and that the response CRC state is reset correctly between commands. No security response is indicated by the commit itself.
Security signals we found
New optional integrity check added to CLI protocol
CRC validation rejects malformed or mismatched checksums with CLI_ERROR_INVALID_CRC
Feature is disabled by default and must be explicitly enabled per session
No buffer overflow or memory safety issue visible in the diff
No mention of CVE, security bug, researcher credit, or advisory in commit
Evidence from the diff
The patch introduces crc-enable/crc-disable commands in the prodtest firmware and extends the reusable cli library to parse and append CRC-32 (poly 0xEDB88320, init/final XOR 0xFFFFFFFF) suffixes on CLI lines. cli.c now strips a trailing <CRC32> when crc_req is true, validates it against the preceding command text, and appends a CRC to every outgoing line. The feature is off by default and only enabled by an explicit prodtest command. It is scoped to the production-test project and does not affect the main wallet firmware runtime.
Changed components
core/embed/rtl/cli.ccore/embed/rtl/inc/rtl/cli.hcore/embed/projects/prodtest/cmd/prodtest_crc.ccore/SConscript.prodtestcore/SConscript.prodtest_emucore/embed/projects/prodtest/README.mdInspect captured patch +200 / −7
diff --git a/core/SConscript.prodtest b/core/SConscript.prodtest
index c889cd812..0c0973daf 100644
--- a/core/SConscript.prodtest
+++ b/core/SConscript.prodtest
@@ -197,6 +197,7 @@ SOURCE_PRODTEST = [
'embed/projects/prodtest/cmd/prodtest_ble.c',
'embed/projects/prodtest/cmd/prodtest_bootloader.c',
'embed/projects/prodtest/cmd/prodtest_button.c',
+ 'embed/projects/prodtest/cmd/prodtest_crc.c',
'embed/projects/prodtest/cmd/prodtest_display.c',
'embed/projects/prodtest/cmd/prodtest_prodtest.c',
'embed/projects/prodtest/cmd/prodtest_backup_ram.c',
diff --git a/core/SConscript.prodtest_emu b/core/SConscript.prodtest_emu
index dca61f1b3..8f9af6ccc 100644
--- a/core/SConscript.prodtest_emu
+++ b/core/SConscript.prodtest_emu
@@ -170,6 +170,7 @@ SOURCE_PRODTEST = [
'embed/projects/prodtest/cmd/prodtest_ble.c',
'embed/projects/prodtest/cmd/prodtest_bootloader.c',
'embed/projects/prodtest/cmd/prodtest_button.c',
+ 'embed/projects/prodtest/cmd/prodtest_crc.c',
'embed/projects/prodtest/cmd/prodtest_display.c',
'embed/projects/prodtest/cmd/prodtest_prodtest.c',
'embed/projects/prodtest/cmd/prodtest_backup_ram.c',
diff --git a/core/embed/projects/prodtest/.changelog.d/6038.added b/core/embed/projects/prodtest/.changelog.d/6038.added
new file mode 100644
index 000000000..8ca52db75
--- /dev/null
+++ b/core/embed/projects/prodtest/.changelog.d/6038.added
@@ -0,0 +1 @@
+Added support for CRC checksums in prodtest commands.
diff --git a/core/embed/projects/prodtest/README.md b/core/embed/projects/prodtest/README.md
index fa9e533f2..7abbc8904 100644
--- a/core/embed/projects/prodtest/README.md
+++ b/core/embed/projects/prodtest/README.md
@@ -79,6 +79,28 @@ rgbled-set 0 255 0
OK
```
+### CRC Checksum
+
+The CLI supports an optional CRC checksum for commands and responses to ensure data integrity over the communication link.
+
+When CRC is enabled, every command MUST include a CRC-32 checksum at the end of the line, preceded by a space.
+
+Command Format:
+`<command> [<args>] <CRC32>`
+
+The checksum is calculated using the standard CRC-32 algorithm (polynomial `0xEDB88320`, initial value `0xFFFFFFFF`, and final XOR `0xFFFFFFFF`) over the command string excluding the ` <CRC32>` suffix.
+
+The device also appends the checksum to every response line (including `OK`, `ERROR`, `PROGRESS`, and `#` traces).
+
+Response Format:
+`<response> <CRC32>`
+
+Example with CRC enabled:
+```
+ping ABC 70417631
+OK ABC E7193F16
+```
+
## List of commands
### help
@@ -272,6 +294,24 @@ display-text hello_world
OK
```
+### crc-enable
+Enables CRC check for CLI commands. Once enabled, the device expects all subsequent commands to include a CRC-32 checksum. The response to `crc-enable` itself already includes the CRC checksum.
+
+Example:
+```
+crc-enable
+OK @C09F27E9
+```
+
+### crc-disable
+Disables CRC check for CLI commands. The command itself must still include the CRC checksum if CRC was previously enabled.
+
+Example:
+```
+crc-disable @939BC008
+OK
+```
+
### display-bars
Draws vertical color bars on the screen according to a specified string of color codes.
diff --git a/core/embed/projects/prodtest/cmd/prodtest_crc.c b/core/embed/projects/prodtest/cmd/prodtest_crc.c
new file mode 100644
index 000000000..efa7238c1
--- /dev/null
+++ b/core/embed/projects/prodtest/cmd/prodtest_crc.c
@@ -0,0 +1,54 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <rtl/cli.h>
+
+static void prodtest_crc_enable(cli_t* cli) {
+ if (cli_arg_count(cli) > 0) {
+ cli_error_arg_count(cli);
+ return;
+ }
+
+ cli_enable_crc(cli);
+ cli_ok(cli, "");
+}
+
+static void prodtest_crc_disable(cli_t* cli) {
+ if (cli_arg_count(cli) > 0) {
+ cli_error_arg_count(cli);
+ return;
+ }
+
+ cli_disable_crc(cli);
+ cli_ok(cli, "");
+}
+
+// clang-format off
+
+PRODTEST_CLI_CMD(
+ .name = "crc-enable",
+ .func = prodtest_crc_enable,
+ .info = "Enables CRC check",
+ .args = "");
+
+PRODTEST_CLI_CMD(
+ .name = "crc-disable",
+ .func = prodtest_crc_disable,
+ .info = "Disables CRC check",
+ .args = "");
diff --git a/core/embed/rtl/cli.c b/core/embed/rtl/cli.c
index 84dd994ba..1ba647623 100644
--- a/core/embed/rtl/cli.c
+++ b/core/embed/rtl/cli.c
@@ -11,12 +11,27 @@
#define ESC_COLOR_GRAY "\e[37m"
#define ESC_COLOR_RESET "\e[39m"
+#define CRC32_INITIAL 0xFFFFFFFF
+#define CRC32_POLYNOMIAL 0xEDB88320
+
+static uint32_t cli_crc32(uint32_t crc, const void* data, size_t size) {
+ const uint8_t* p = (const uint8_t*)data;
+ while (size--) {
+ crc ^= *p++;
+ for (int i = 0; i < 8; i++) {
+ crc = (crc >> 1) ^ (CRC32_POLYNOMIAL & (-(crc & 1)));
+ }
+ }
+ return crc;
+}
+
bool cli_init(cli_t* cli, cli_read_cb_t read, cli_write_cb_t write,
void* callback_context) {
memset(cli, 0, sizeof(cli_t));
cli->read = read;
cli->write = write;
cli->callback_context = callback_context;
+ cli->response_crc = CRC32_INITIAL;
return true;
}
@@ -29,8 +44,19 @@ void cli_set_commands(cli_t* cli, const cli_command_t* cmd_array,
static void cli_vprintf(cli_t* cli, const char* format, va_list args) {
char buffer[CLI_LINE_BUFFER_SIZE];
- vsnprintf_(buffer, sizeof(buffer), format, args);
- cli->write(cli->callback_context, buffer, strlen(buffer));
+ int len = vsnprintf_(buffer, sizeof(buffer), format, args);
+ if (len < 0) return;
+
+ size_t write_len = (size_t)len;
+ if (write_len >= sizeof(buffer)) {
+ write_len = sizeof(buffer) - 1;
+ }
+
+ if (cli->crc_req) {
+ cli->response_crc = cli_crc32(cli->response_crc, buffer, write_len);
+ }
+
+ cli->write(cli->callback_context, buffer, write_len);
}
static void cli_printf(cli_t* cli, const char* format, ...) {
@@ -40,6 +66,20 @@ static void cli_printf(cli_t* cli, const char* format, ...) {
va_end(args);
}
+static void cli_printf_newline(cli_t* cli) {
+ if (cli->crc_req) {
+ uint32_t final_crc = ~cli->response_crc;
+ cli->crc_req = false;
+ cli_printf(cli, " %08X", final_crc);
+ cli->crc_req = true;
+ }
+ bool old_crc_req = cli->crc_req;
+ cli->crc_req = false;
+ cli_printf(cli, "\r\n");
+ cli->crc_req = old_crc_req;
+ cli->response_crc = CRC32_INITIAL;
+}
+
void cli_vtrace(cli_t* cli, const char* format, va_list args) {
if (cli->interactive) {
cli_printf(cli, ESC_COLOR_GRAY);
@@ -57,7 +97,7 @@ void cli_vtrace(cli_t* cli, const char* format, va_list args) {
cli_vprintf(cli, format, args);
}
- cli_printf(cli, "\r\n");
+ cli_printf_newline(cli);
}
void cli_trace(cli_t* cli, const char* format, ...) {
@@ -87,7 +127,7 @@ void cli_ok(cli_t* cli, const char* format, ...) {
cli_vprintf(cli, format, args);
va_end(args);
}
- cli_printf(cli, "\r\n");
+ cli_printf_newline(cli);
cli->final_status = true;
}
@@ -110,7 +150,7 @@ void cli_ok_hexdata(cli_t* cli, const void* data, size_t size) {
cli_printf(cli, "%02X", ((uint8_t*)data)[i]);
}
}
- cli_printf(cli, "\r\n");
+ cli_printf_newline(cli);
cli->final_status = true;
}
@@ -136,7 +176,7 @@ static void cli_verror(cli_t* cli, const char* code, const char* format,
cli_printf(cli, "\"");
}
- cli_printf(cli, "\r\n");
+ cli_printf_newline(cli);
cli->final_status = true;
}
@@ -180,7 +220,7 @@ void cli_progress(cli_t* cli, const char* format, ...) {
cli_vprintf(cli, format, args);
}
- cli_printf(cli, "\r\n");
+ cli_printf_newline(cli);
va_end(args);
}
@@ -189,6 +229,10 @@ void cli_abort(cli_t* cli) { cli->aborted = true; }
bool cli_aborted(cli_t* cli) { return cli->aborted; }
+void cli_enable_crc(cli_t* cli) { cli->crc_req = true; }
+
+void cli_disable_crc(cli_t* cli) { cli->crc_req = false; }
+
// Finds a command record by name
//
// Returns NULL if the command is not found
@@ -496,6 +540,7 @@ static void cli_clear_line(cli_t* cli) {
cli->line_cursor = 0;
cli->hist_idx = 0;
cli->hist_prefix = 0;
+ cli->response_crc = CRC32_INITIAL;
memset(cli->line_buffer, 0, sizeof(cli->line_buffer));
}
@@ -539,6 +584,7 @@ void cli_process_command(cli_t* cli, const cli_command_t* cmd) {
cli->current_cmd = cmd;
cli->final_status = false;
cli->aborted = false;
+ cli->response_crc = CRC32_INITIAL;
// Call the command handler
cmd->func(cli);
@@ -577,6 +623,37 @@ const cli_command_t* cli_process_io(cli_t* cli) {
goto cleanup;
}
+ // Handle optional CRC check
+ if (cli->crc_req) {
+ size_t len = strlen(cli->line_buffer);
+ if (len >= 9) {
+ char* space = &cli->line_buffer[len - 9];
+ if (*space == ' ') {
+ uint32_t received_crc;
+ if (cstr_parse_uint32(space + 1, 16, &received_crc)) {
+ uint32_t calculated_crc =
+ ~cli_crc32(CRC32_INITIAL, cli->line_buffer, len - 9);
+ if (calculated_crc == received_crc) {
+ *space = '\0';
+ } else {
+ cli_error(cli, CLI_ERROR_INVALID_CRC, "Expected %08X, got %08X",
+ calculated_crc, received_crc);
+ goto cleanup;
+ }
+ } else {
+ cli_error(cli, CLI_ERROR_INVALID_CRC, "Invalid CRC format");
+ goto cleanup;
+ }
+ } else {
+ cli_error(cli, CLI_ERROR_INVALID_CRC, "CRC suffix missing");
+ goto cleanup;
+ }
+ } else {
+ cli_error(cli, CLI_ERROR_INVALID_CRC, "Line too short for CRC");
+ goto cleanup;
+ }
+ }
+
cli_history_add(cli, cli->line_buffer);
// Split command line into arguments
diff --git a/core/embed/rtl/inc/rtl/cli.h b/core/embed/rtl/inc/rtl/cli.h
index b8d6e400f..87bc96666 100644
--- a/core/embed/rtl/inc/rtl/cli.h
+++ b/core/embed/rtl/inc/rtl/cli.h
@@ -140,6 +140,11 @@ struct cli {
* finish as soon as possible with an CLI_ERROR_ABORT
*/
volatile bool aborted;
+
+ /** CRC of the current response line */
+ uint32_t response_crc;
+ /** CRC was requested for the current command */
+ bool crc_req;
};
/** Initializes the command line structure */
@@ -248,3 +253,17 @@ void cli_abort(cli_t* cli);
/** Returns true if `cli_abort()` was called */
bool cli_aborted(cli_t* cli);
+
+/**
+ * Enables CRC check for the CLI.
+ *
+ * When enabled, every line received from the host must be suffixed with
+ * " <CRC32>" and every line sent to the host will be suffixed with
+ * " <CRC32>".
+ */
+void cli_enable_crc(cli_t* cli);
+
+/**
+ * Disables CRC check for the CLI.
+ */
+void cli_disable_crc(cli_t* cli);
Why this scored 21/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.