docs(thp): add low-order point verification to CodeEntry
What changed, and why it matters
This commit updates a design document for Trezor's new 'Trezor Hello Protocol' (THP) pairing flow. It adds checks intended to stop a malicious computer from sending specially crafted X25519 public keys (so-called low-order points) that would make the shared secret predictable or all-zeroes, potentially allowing the computer to skip the user code-entry step and pair with the device without authorization. The change is only to documentation/specification; no actual firmware code is modified in this commit, so the real-world risk depends on whether the implementation already follows or ignores the new checks.
Verify that the firmware implementation of ThpCodeEntryCpaceHostTag already enforces both assertions (non-zero host public key and non-zero shared secret) before this specification change is considered complete. If not, file a security issue and patch the implementation. Also add test vectors covering low-order and all-zero X25519 points for the CodeEntry flow.
Security signals we found
X25519 low-order point authentication bypass
CPace code-entry pairing protocol
all-zero public key / shared secret rejection
documentation-only change to security protocol
commit message explicitly calls out bypass prevention
Evidence from the diff
The diff modifies docs/common/thp/specification.md, adding two assertions to the CodeEntry CPace sub-protocol: (1) reject an all-zero cpace_host_public_key, and (2) reject an all-zero shared_secret produced by X25519. The commit message and inline note explicitly state this ‘prevents the host from using low-order points to bypass code entry authentication.’ Low-order X25519 points can force the ECDH shared secret to a known small set of values (including all-zero), which would let a host compute the SHA-256 tag without knowing the user-entered code, defeating the pairing authentication. Because the patch is documentation-only, it does not by itself fix any running firmware; it either documents a requirement that code must satisfy, or retroactively specifies a missing defense. Confidence is limited because we cannot see the corresponding implementation.
Changed components
docs/common/thp/specification.mdTrezor Hello Protocol (THP) CodeEntry pairing flowX25519/CPace key exchange in THPInspect captured patch +9 / −7
diff --git a/docs/common/thp/specification.md b/docs/common/thp/specification.md
index 2b9a8585..a6fc5c7d 100644
--- a/docs/common/thp/specification.md
+++ b/docs/common/thp/specification.md
@@ -876,10 +876,12 @@ The behavior of Trezor in the state **TP2** is defined as follows:
- When the message ThpSelectMethod(*selected_pairing_method*) is received, transition to the intermediate state “selected method”.
- When the message ThpCodeEntryCpaceHostTag(*cpace_host_public_key*, *tag*) is received, take the following actions:
1. Clear the screen.
- 2. Set *shared_secret* = X25519(*cpace_trezor_private_key*, *cpace_host_public_key*).
- 3. Assert that *tag* == SHA-256(*shared_secret*).
- 4. Send the message ThpCodeEntrySecret(*code_entry_secret*) to the host.
- 5. Transition to the state **TC1**.
+ 2. Assert that *cpace_host_public_key* != 0x00 ^ 32.
+ 3. Set *shared_secret* = X25519(*cpace_trezor_private_key*, *cpace_host_public_key*).
+ 4. Assert that *shared_secret* != 0x00 ^ 32. This prevents the host from using low-order points to bypass code entry authentication.
+ 5. Assert that *tag* == SHA-256(*shared_secret*).
+ 6. Send the message ThpCodeEntrySecret(*code_entry_secret*) to the host.
+ 7. Transition to the state **TC1**.
#### State TP3b
@@ -1104,7 +1106,7 @@ sequenceDiagram
Trezor -->> host - first time: code (user rewrites code from Trezor to host)
note over host - first time: pregenerator = sha512(prefix || code || padding || handshake_hash_H || 0x00)[:32]<br>generator = ELLIGATOR2(pregenerator)<br>cpace_host_private_key = random_bytes(32)<br>cpace_host_public_key = X25519(cpace_host_private_key, generator)<br>shared_secret = X25519(cpace_host_private_key, cpace_trezor_public_key)<br>tag=sha256(shared_secret)
host - first time ->> Trezor: cpace_host_public_key, tag
- note over Trezor: shared_secret = X25519(cpace_trezor_private_key, cpace_host_public_key)
+ note over Trezor: assert cpace_host_public_key != 0x00 ^ 32<br>shared_secret = X25519(cpace_trezor_private_key, cpace_host_public_key)<br>assert shared_secret != 0x00 ^ 32
note over Trezor: assert tag == sha256(shared_secret)
Trezor ->> host - first time: secret
note over host - first time: assert commitment == sha256(secret)
@@ -1122,7 +1124,7 @@ sequenceDiagram
Trezor -->> host - second (or more) time : code (user rewrites code from Trezor to host)
note over host - second (or more) time : pregenerator = sha512(prefix || code || padding || handshake_hash_H || 0x00)[:32]<br>generator = ELLIGATOR2(pregenerator)<br>cpace_host_private_key = random_bytes(32)<br>cpace_host_public_key = X25519(cpace_host_private_key, generator)<br>shared_secret = X25519(cpace_host_private_key, cpace_trezor_public_key)<br>tag=sha256(shared_secret)
host - second (or more) time ->> Trezor: cpace_host_public_key, tag
- note over Trezor: shared_secret = X25519(cpace_trezor_private_key, cpace_host_public_key)
+ note over Trezor: assert cpace_host_public_key != 0x00 ^ 32<br>shared_secret = X25519(cpace_trezor_private_key, cpace_host_public_key)<br>assert shared_secret != 0x00 ^ 32
note over Trezor: assert tag == sha256(shared_secret)
Trezor ->> host - second (or more) time : secret
note over host - second (or more) time : assert commitment == sha256(secret)
@@ -1152,7 +1154,7 @@ sequenceDiagram
Trezor -->> host: code (user rewrites code from Trezor to host)
note over host: pregenerator = sha512(prefix || code || padding || handshake_hash_H || 0x00)[:32]<br>generator = ELLIGATOR2(pregenerator)<br>cpace_host_private_key = random_bytes(32)<br>cpace_host_public_key = X25519(cpace_host_private_key, generator)<br>shared_secret = X25519(cpace_host_private_key, cpace_trezor_public_key)<br>tag=sha256(shared_secret)
host ->> Trezor: cpace_host_public_key, tag
- note over Trezor: shared_secret = X25519(cpace_trezor_private_key, cpace_host_public_key)
+ note over Trezor: assert cpace_host_public_key != 0x00 ^ 32<br>shared_secret = X25519(cpace_trezor_private_key, cpace_host_public_key)<br>assert shared_secret != 0x00 ^ 32
note over Trezor: assert tag == sha256(shared_secret)
Trezor ->> host: secret
note over host: assert commitment == sha256(secret)
Why this scored 57/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.