feat(core/prodtest): set HSM public keys
What changed, and why it matters
This commit updates Trezor's production-test firmware to accept multiple trusted public keys from secure hardware modules (HSMs) instead of just one. It also adds a helper function in the Noise crypto library that tries each trusted key in turn during a secure handshake. The change itself is a feature addition, not an obvious bug fix. However, the helper function restores a sensitive temporary key from a backup on every retry, which is good practice, and it wipes the backup when done. There is no direct evidence in the commit that this fixes a security vulnerability; it looks like operational hardening for factory testing.
Treat as routine production-test hardening. Review whether the public-key list length is bounded and whether `responder_public_keys_count` can be influenced by an attacker; the current call site uses a compile-time constant array, so exposure is limited. If the helper is later exposed to untrusted callers, ensure count validation and constant-time handling are considered. No immediate security patch action is indicated by the diff alone.
Security signals we found
Addition of multiple trusted responder public keys for HSM pairing in production-test code
New Noise helper that iterates over a list of responder public keys and restores the initiator ephemeral private key before each attempt
Use of `memzero` to clear the ephemeral key backup after a successful handshake
Hard-coded Curve25519 public keys embedded in firmware for production and non-production builds
No changelog entry and no vendor description of security relevance
Evidence from the diff
The patch modifies core/embed/projects/prodtest/cmd/secure_channel.c to replace a single hard-coded hsm_public_key with an array hsm_public_keys[] containing two production keys and one non-production key selected by the PRODUCTION macro. It switches the handshake call from noise_handle_handshake_response to a new noise_handle_handshake_response_multiple_keys wrapper. In crypto/noise.c, the wrapper backs up ctx->initiator_ephemeral_private_key, restores it before each trial key, calls the existing single-key handler, and memzeros the backup on success. crypto/noise.h declares the new function. The commit is tagged [no changelog] and contains no security-related explanation.
Changed components
core/embed/projects/prodtest/cmd/secure_channel.ccrypto/noise.ccrypto/noise.hTrezor production-test secure-channel HSM pairingInspect captured patch +48 / −9
diff --git a/core/embed/projects/prodtest/cmd/secure_channel.c b/core/embed/projects/prodtest/cmd/secure_channel.c
index a790f1c9..559e8b2f 100644
--- a/core/embed/projects/prodtest/cmd/secure_channel.c
+++ b/core/embed/projects/prodtest/cmd/secure_channel.c
@@ -19,6 +19,8 @@
#include "secure_channel.h"
+#include "memzero.h"
+
#include "string.h"
typedef enum {
@@ -38,11 +40,20 @@ static curve25519_key prodtest_private_key = {
0xc8, 0x56, 0x36, 0x89, 0xf5, 0xa6, 0x70, 0x66, 0x43, 0xeb, 0xe3,
0x7e, 0xff, 0x7a, 0x2c, 0x20, 0x31, 0x27, 0x58, 0xbe, 0x5f, 0x01,
0xc8, 0x6f, 0x9b, 0xe7, 0xe2, 0xe6, 0x0b, 0xee, 0x7e, 0x55};
-// TODO: Generate the key on HSM and use it here.
-static curve25519_key hsm_public_key = {
- 0xcf, 0xce, 0x80, 0xf7, 0xc8, 0x7e, 0xa1, 0xe9, 0x3d, 0x0d, 0x80,
- 0x98, 0x3f, 0xec, 0xc9, 0x98, 0xa0, 0xdd, 0xb6, 0xaa, 0x7a, 0x36,
- 0x36, 0x6b, 0x6c, 0x7d, 0xd4, 0x09, 0x32, 0x5f, 0x67, 0x4b};
+static curve25519_key hsm_public_keys[] = {
+#if PRODUCTION
+ {0xba, 0x79, 0x2d, 0x15, 0xc6, 0x87, 0xb4, 0xa5, 0x31, 0xbe, 0x20,
+ 0x1e, 0x88, 0x73, 0x86, 0xaa, 0x7b, 0x9f, 0x24, 0x09, 0x2d, 0xb7,
+ 0x7a, 0xc9, 0x5b, 0x84, 0xee, 0xb3, 0x36, 0x66, 0x47, 0x5c},
+ {0x07, 0x7a, 0xe8, 0xf9, 0xf8, 0x83, 0x9a, 0x8f, 0x07, 0x73, 0xc4,
+ 0x98, 0x89, 0x40, 0x21, 0x86, 0xf9, 0xf9, 0xa8, 0xf4, 0xb2, 0xe7,
+ 0xac, 0x0e, 0xe5, 0x83, 0xa2, 0xf7, 0xe6, 0x63, 0x82, 0x54}
+#else
+ {0xcf, 0xce, 0x80, 0xf7, 0xc8, 0x7e, 0xa1, 0xe9, 0x3d, 0x0d, 0x80,
+ 0x98, 0x3f, 0xec, 0xc9, 0x98, 0xa0, 0xdd, 0xb6, 0xaa, 0x7a, 0x36,
+ 0x36, 0x6b, 0x6c, 0x7d, 0xd4, 0x09, 0x32, 0x5f, 0x67, 0x4b}
+#endif
+};
bool secure_channel_handshake_1(uint8_t output[SECURE_CHANNEL_OUTPUT_SIZE]) {
if (!noise_create_handshake_request(&noise_context,
@@ -61,14 +72,14 @@ bool secure_channel_handshake_2(
return false;
}
- if (!noise_handle_handshake_response(&noise_context, prodtest_private_key,
- hsm_public_key,
- (const noise_response_t*)input)) {
+ if (!noise_handle_handshake_response_multiple_keys(
+ &noise_context, prodtest_private_key, hsm_public_keys,
+ sizeof(hsm_public_keys) / sizeof(hsm_public_keys[0]),
+ (const noise_response_t*)input)) {
return false;
}
noise_state = SECURE_CHANNEL_STATE_2;
-
return true;
}
diff --git a/crypto/noise.c b/crypto/noise.c
index 332d0331..b50dec8f 100644
--- a/crypto/noise.c
+++ b/crypto/noise.c
@@ -335,3 +335,22 @@ bool noise_receive_message(noise_context_t *ctx, const uint8_t *associated_data,
}
return true;
}
+
+bool noise_handle_handshake_response_multiple_keys(
+ noise_context_t *ctx, const curve25519_key initiator_private_key,
+ const curve25519_key responder_public_keys[],
+ size_t responder_public_keys_count, const noise_response_t *response) {
+ curve25519_key ephemeral_key_backup = {0};
+ memcpy(ephemeral_key_backup, ctx->initiator_ephemeral_private_key,
+ sizeof(ephemeral_key_backup));
+ for (size_t i = 0; i < responder_public_keys_count; i++) {
+ memcpy(ctx->initiator_ephemeral_private_key, ephemeral_key_backup,
+ sizeof(ephemeral_key_backup));
+ if (noise_handle_handshake_response(ctx, initiator_private_key,
+ responder_public_keys[i], response)) {
+ memzero(ephemeral_key_backup, sizeof(ephemeral_key_backup));
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/crypto/noise.h b/crypto/noise.h
index 2b7cd36c..86869343 100644
--- a/crypto/noise.h
+++ b/crypto/noise.h
@@ -75,6 +75,15 @@ bool noise_handle_handshake_response(noise_context_t* ctx,
const curve25519_key responder_public_key,
const noise_response_t* response);
+// This is called by the initiator to handle the handshake response
+// This is a wrapper above noise_handle_handshake_response that allows to pass
+// multiple responder public keys, the first key that succeeds in paring is
+// used
+bool noise_handle_handshake_response_multiple_keys(
+ noise_context_t* ctx, const curve25519_key initiator_private_key,
+ const curve25519_key responder_public_keys[],
+ size_t responder_public_keys_count, const noise_response_t* response);
+
// This is called by both the initiator and responder to send a message
// len(ciphertext) == plaintext_length + NOISE_TAG_SIZE
// The official Noise specification requires the associated_data to be empty
Why this scored 25/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.