auth: add unattended pin selection for rpc tests
What changed, and why it matters
This commit adds a debug-only feature that lets automated tests set or change the device's PIN over an internal RPC message. It only compiles when special debug flags are enabled and is not present in normal production firmware. The change itself is a test helper, but it touches the sensitive PIN-entry path, so it deserves careful review to ensure it cannot accidentally end up in a shipping build.
Verify that CONFIG_DEBUG_UNATTENDED_CI is never enabled in production or release firmware builds, and that the new debug_set_pin RPC is rejected or absent in those configurations. Review that the debug PIN context cannot influence normal PIN entry when the flag is disabled. No immediate patch is required if the feature is strictly test-only.
Security signals we found
New RPC endpoint debug_set_pin can set/change the unlock PIN
PIN-handling code path modified to read from a mutable debug context instead of a fixed constant
Code is wrapped in CONFIG_DEBUG_UNATTENDED_CI and CONFIG_DEBUG_MODE preprocessor guards
Default test PIN changed from 012345 to 12345
No production release or security advisory referenced in commit materials
Evidence from the diff
The patch introduces CONFIG_DEBUG_UNATTENDED_CI-gated code that stores a test PIN context (debug_pin_ctx_t) and replaces the hardcoded 012345 test PIN with a configurable one. It adds a new RPC method debug_set_pin that accepts pin and optional new_pin parameters, validates them against DIGIT_ENTRY_UINT_MAX, and updates the context. Existing get_pin_get_aeskey and set_pin_get_aeskey use the context PIN when the unattended CI flag is set. A default DEBUG_CI_DEFAULT_PIN of 12345 is defined. The feature is wired into the debug message dispatcher and reset routine.
Changed components
main/process/auth_user.cmain/process/dashboard.cmain/process/debug_clean.cmain/process/process_utils.hmain/ui.hInspect captured patch +100 / −20
### main/process/auth_user.c
@@ -27,7 +27,68 @@ bool pinclient_set(
// Whether we want to change the PIN on the next unlock
static bool change_pin_requested = false;
-void set_request_change_pin(const bool change_pin) { change_pin_requested = change_pin; }
+#ifdef CONFIG_DEBUG_UNATTENDED_CI
+typedef struct {
+ uint32_t pin;
+ uint32_t new_pin;
+ bool has_new_pin;
+} debug_pin_ctx_t;
+static debug_pin_ctx_t debug_pin_ctx = { .pin = DEBUG_CI_DEFAULT_PIN };
+#endif
+
+void set_request_change_pin(const bool change_pin)
+{
+ change_pin_requested = change_pin;
+#ifdef CONFIG_DEBUG_UNATTENDED_CI
+ if (!change_pin) {
+ debug_pin_ctx.has_new_pin = false;
+ }
+#endif
+}
+
+#ifdef CONFIG_DEBUG_MODE
+#ifdef CONFIG_DEBUG_UNATTENDED_CI
+static void debug_copy_pin(const uint32_t pin, digit_entry_t* digit_entry_out)
+{
+ char digits[sizeof(digit_entry_out->digit) + 1]; // Nul terminated
+ const int ret = snprintf(digits, sizeof(digits), "%06" PRIu32, pin);
+ JADE_ASSERT(ret == sizeof(digits) - 1);
+ memcpy(digit_entry_out->digit, digits, ret);
+}
+
+void debug_set_pin(const uint32_t pin)
+{
+ JADE_ASSERT(pin <= DIGIT_ENTRY_UINT_MAX);
+ debug_pin_ctx.pin = pin;
+ set_request_change_pin(false);
+}
+
+void debug_set_pin_process(void* process_ptr)
+{
+ jade_process_t* process = process_ptr;
+ ASSERT_CURRENT_MESSAGE(process, "debug_set_pin");
+ GET_MSG_PARAMS(process);
+
+ debug_pin_ctx_t ctx;
+ if (!rpc_get_uint32("pin", ¶ms, &ctx.pin) || ctx.pin > DIGIT_ENTRY_UINT_MAX) {
+ jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, "Invalid pin");
+ goto cleanup;
+ }
+
+ ctx.has_new_pin = rpc_get_uint32("new_pin", ¶ms, &ctx.new_pin);
+ if (ctx.has_new_pin && ctx.new_pin > DIGIT_ENTRY_UINT_MAX) {
+ jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, "Invalid new_pin");
+ goto cleanup;
+ }
+
+ debug_pin_ctx = ctx;
+ set_request_change_pin(ctx.has_new_pin);
+ jade_process_reply_to_message_ok(process);
+cleanup:
+ return;
+}
+#endif // CONFIG_DEBUG_UNATTENDED_CI
+#endif // CONFIG_DEBUG_MODE
static void check_wallet_erase_pin(jade_process_t* process, const uint8_t* pin_entered, const size_t pin_len)
{
@@ -52,12 +113,9 @@ static void check_wallet_erase_pin(jade_process_t* process, const uint8_t* pin_e
static bool get_pin_get_aeskey(jade_process_t* process, const char* title, uint8_t* pin, const size_t pin_len,
uint8_t* aeskey, const size_t aes_len)
{
- JADE_ASSERT(process);
- JADE_ASSERT(title);
- JADE_ASSERT(pin);
- JADE_ASSERT(pin_len == DIGIT_ENTRY_SIZE);
- JADE_ASSERT(aeskey);
- JADE_ASSERT(aes_len == AES_KEY_LEN_256);
+ JADE_ASSERT(process && title);
+ JADE_ASSERT(pin && pin_len == DIGIT_ENTRY_SIZE);
+ JADE_ASSERT(aeskey && aes_len == AES_KEY_LEN_256);
// At this point we should have encrypted keys persisted in the flash
JADE_ASSERT(keychain_has_pin());
@@ -87,7 +145,6 @@ static bool get_pin_get_aeskey(jade_process_t* process, const char* title, uint8
// If getting PIN via QRs, free gui memory before attempting QR roundtrip
gui_set_current_activity_ex(digit_entry.activity, process->ctx.source == SOURCE_INTERNAL);
- // In a debug unattended ci build, use hardcoded pin after a short delay
#ifndef CONFIG_DEBUG_UNATTENDED_CI
if (!run_digit_entry_loop(&digit_entry)) {
// User abandoned entering pin
@@ -96,9 +153,9 @@ static bool get_pin_get_aeskey(jade_process_t* process, const char* title, uint8
return false;
}
#else
+ // Debug unattended ci build: use hardcoded pin after a short delay
vTaskDelay(CONFIG_DEBUG_UNATTENDED_CI_TIMEOUT_MS / portTICK_PERIOD_MS);
- const uint8_t testpin[sizeof(digit_entry.digit)] = { 0, 1, 2, 3, 4, 5 };
- memcpy(digit_entry.digit, testpin, sizeof(testpin));
+ debug_copy_pin(debug_pin_ctx.pin, &digit_entry);
#endif
memcpy(pin, digit_entry.digit, sizeof(digit_entry.digit));
SENSITIVE_POP(&digit_entry);
@@ -114,15 +171,11 @@ static bool get_pin_get_aeskey(jade_process_t* process, const char* title, uint8
static bool set_pin_get_aeskey(jade_process_t* process, const char* title, uint8_t* pin, const size_t pin_len,
uint8_t* aeskey, const size_t aes_len)
{
- JADE_ASSERT(process);
- JADE_ASSERT(title);
- JADE_ASSERT(pin);
- JADE_ASSERT(pin_len == DIGIT_ENTRY_SIZE);
- JADE_ASSERT(aeskey);
- JADE_ASSERT(aes_len == AES_KEY_LEN_256);
+ JADE_ASSERT(process && title);
+ JADE_ASSERT(pin && pin_len == DIGIT_ENTRY_SIZE);
+ JADE_ASSERT(aeskey && aes_len == AES_KEY_LEN_256);
// Enter PIN to lock mnemonic/key material.
- // In a debug unattended ci build, use hardcoded pin after a short delay
digit_entry_t digit_entry = { .entry_type = DIGIT_ENTRY_PIN, .initial_state = RANDOM, .digits_shown = false };
JADE_ASSERT(sizeof(digit_entry.digit) == pin_len);
make_digit_entry_activity(&digit_entry, title, NULL);
@@ -143,9 +196,9 @@ static bool set_pin_get_aeskey(jade_process_t* process, const char* title, uint8
return false;
}
#else
+ // Debug unattended ci build: use hardcoded pin after a short delay
vTaskDelay(CONFIG_DEBUG_UNATTENDED_CI_TIMEOUT_MS / portTICK_PERIOD_MS);
- const uint8_t testpin[sizeof(digit_entry.digit)] = { 0, 1, 2, 3, 4, 5 };
- memcpy(digit_entry.digit, testpin, sizeof(testpin));
+ debug_copy_pin(debug_pin_ctx.has_new_pin ? debug_pin_ctx.new_pin : debug_pin_ctx.pin, &digit_entry);
#endif
// this is the first pin, copy it and clear screen fields and have the user confirm
@@ -158,8 +211,9 @@ static bool set_pin_get_aeskey(jade_process_t* process, const char* title, uint8
continue;
}
#else
+ // Debug unattended ci build: copy back the original pin for checking
vTaskDelay(CONFIG_DEBUG_UNATTENDED_CI_TIMEOUT_MS / portTICK_PERIOD_MS);
- memcpy(digit_entry.digit, testpin, sizeof(testpin));
+ memcpy(digit_entry.digit, pin, sizeof(digit_entry.digit));
#endif
// check that the two pins are the same
@@ -304,6 +358,10 @@ static bool set_pin_save_keys(jade_process_t* process)
uint8_t aeskey[AES_KEY_LEN_256];
SENSITIVE_PUSH(aeskey, sizeof(aeskey));
+#ifdef CONFIG_DEBUG_UNATTENDED_CI
+ debug_pin_ctx.has_new_pin = false; // Ignore pending test PIN change during wallet setup
+#endif
+
// Do the pinserver 'setpin' process
if (!set_pin_get_aeskey(process, "Enter New PIN", pin, sizeof(pin), aeskey, sizeof(aeskey))) {
// User abandoned entering PIN, or some sort of server or networking/connection error
### main/process/dashboard.c
@@ -157,6 +157,9 @@ void debug_scan_qr_process(void* process_ptr);
void debug_set_mnemonic_process(void* process_ptr);
void debug_clean_reset_process(void* process_ptr);
void debug_handshake(void* process_ptr);
+#ifdef CONFIG_DEBUG_UNATTENDED_CI
+void debug_set_pin_process(void* process_ptr);
+#endif
bool debug_selfcheck(jade_process_t* process);
#endif
void ota_process(void* process_ptr);
@@ -537,6 +540,11 @@ static void dispatch_message(jade_process_t* process)
task_function = debug_set_mnemonic_process;
} else if (IS_METHOD("debug_handshake")) {
task_function = debug_handshake;
+
+#ifdef CONFIG_DEBUG_UNATTENDED_CI
+ } else if (IS_METHOD("debug_set_pin")) {
+ task_function = debug_set_pin_process;
+#endif
} else if (IS_METHOD("debug_scan_qr")) {
task_function = debug_scan_qr_process;
} else if (IS_METHOD("get_bip85_bip39_entropy")) {
### main/process/debug_clean.c
@@ -11,6 +11,11 @@
#include "process_utils.h"
#ifdef CONFIG_DEBUG_MODE
+
+#ifdef CONFIG_DEBUG_UNATTENDED_CI
+void debug_set_pin(uint32_t pin);
+#endif
+
void debug_clean_reset_process(void* process_ptr)
{
JADE_LOGI("Starting: %d", xPortGetFreeHeapSize());
@@ -31,6 +36,10 @@ void debug_clean_reset_process(void* process_ptr)
storage_erase_pinserver_cert();
storage_erase_pinserver_details(STORAGE_PIN_ERASE_PRIVKEY);
+#ifdef CONFIG_DEBUG_UNATTENDED_CI
+ debug_set_pin(DEBUG_CI_DEFAULT_PIN);
+#endif
+
// Clean multisig registrations from storage
char multisig_names[MAX_MULTISIG_REGISTRATIONS][NVS_KEY_NAME_MAX_SIZE]; // Sufficient
const size_t num_multsig_names = sizeof(multisig_names) / sizeof(multisig_names[0]);
### main/process/process_utils.h
@@ -17,6 +17,10 @@
#define COMMITMENTS_ASSET_BLIND_PROOF 0x8
#define COMMITMENTS_VALUE_BLIND_PROOF 0x10
+#ifdef CONFIG_DEBUG_UNATTENDED_CI
+#define DEBUG_CI_DEFAULT_PIN 12345
+#endif
+
// Holds asset/value blinding data
typedef struct {
uint64_t value;
### main/ui.h
@@ -38,6 +38,7 @@ typedef struct {
// digit entry
#define DIGIT_ENTRY_SIZE 6
+#define DIGIT_ENTRY_UINT_MAX 999999
#define DIGIT_ENTRY_WORD_NUMBER_SIZE 4
enum __attribute__((__packed__)) digit_entry_type {Why this scored 20/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.