chore(core): add consteq check directly into aesgcm.finish when decrypting
What changed, and why it matters
This commit hardens how Trezor devices verify AES-GCM authentication tags during decryption. Previously, callers computed the tag and then compared it separately using a constant-time helper. Now the comparison is performed inside the cryptographic finish routine itself, using a constant-time equality check. This reduces the risk that a future caller forgets to verify the tag or uses a non-constant-time comparison, which could allow an attacker to tamper with encrypted messages or recover secrets through timing analysis.
Review other AES-GCM callers in the codebase to ensure they also use the new finish(expected_tag) API and do not perform tag verification separately. Confirm that consteq() is implemented correctly and that the tag buffer is cleared on authentication failure to avoid leaking the computed tag.
Security signals we found
AES-GCM tag verification moved into the cryptographic primitive
Constant-time comparison (consteq) enforced for authentication tags
expected_tag made mandatory during decryption state
Manual tag comparisons removed from Python callers
Potential defense against forgotten or timing-vulnerable tag checks in future code
Evidence from the diff
The patch modifies modtrezorcrypto-aesgcm.h so that AesGcm.finish() accepts an optional expected_tag argument. When decrypting, the argument is required; the routine computes the 16-byte GCM tag and compares it to expected_tag using consteq(), raising RuntimeError on mismatch. The Python THP crypto layer (core/src/trezor/wire/thp/crypto.py) is updated to pass the tag directly and treat the resulting exception as authentication failure, removing the previous manual utils.consteq() comparisons. The change is defensive: it centralizes tag verification and enforces constant-time comparison at the C extension boundary.
Changed components
core/embed/upymod/modtrezorcrypto/modtrezorcrypto-aesgcm.hcore/src/trezor/wire/thp/crypto.pycore/mocks/generated/trezorcrypto/__init__.pyiInspect captured patch +44 / −15
diff --git a/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-aesgcm.h b/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-aesgcm.h
index 9f9eb7ae..ba76978a 100644
--- a/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-aesgcm.h
+++ b/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-aesgcm.h
@@ -20,6 +20,7 @@
#include "py/objstr.h"
#include "aes/aesgcm.h"
+#include "consteq.h"
#include "memzero.h"
/// package: trezorcrypto.__init__
@@ -53,7 +54,7 @@ STATIC mp_obj_t mod_trezorcrypto_AesGcm_make_new(const mp_obj_type_t *type,
mp_get_buffer_raise(args[1], &iv, MP_BUFFER_READ);
if (key.len != 16 && key.len != 24 && key.len != 32) {
mp_raise_ValueError(MP_ERROR_TEXT(
- "Invalid length of key (has to be 128, 192 or 256 bits)"));
+ "Invalid length of key (has to be 128, 192 or 256 bits)."));
}
mp_obj_AesGcm_t *o = m_new_obj_with_finaliser(mp_obj_AesGcm_t);
@@ -202,16 +203,24 @@ STATIC mp_obj_t mod_trezorcrypto_AesGcm_auth(mp_obj_t self, mp_obj_t data) {
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_AesGcm_auth_obj,
mod_trezorcrypto_AesGcm_auth);
-/// def finish(self) -> bytes:
+/// def finish(self, expected_tag: AnyBytes | None = None) -> bytes:
/// """
-/// Compute GCM authentication tag.
+/// Compute GCM authentication tag. The `expected_tag` is required when
+/// decrypting.
/// """
-STATIC mp_obj_t mod_trezorcrypto_AesGcm_finish(mp_obj_t self) {
- mp_obj_AesGcm_t *o = MP_OBJ_TO_PTR(self);
+STATIC mp_obj_t mod_trezorcrypto_AesGcm_finish(size_t n_args,
+ const mp_obj_t *args) {
+ mp_obj_AesGcm_t *o = MP_OBJ_TO_PTR(args[0]);
if (o->state != STATE_INIT && o->state != STATE_ENCRYPTING &&
o->state != STATE_DECRYPTING) {
mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Invalid state."));
}
+ if (n_args == 1 && o->state == STATE_DECRYPTING) {
+ mp_raise_msg(
+ &mp_type_RuntimeError,
+ MP_ERROR_TEXT("Argument `expected_tag` is required when decrypting."));
+ }
+
o->state = STATE_FINISHED;
vstr_t tag = {0};
vstr_init_len(&tag, 16);
@@ -220,10 +229,24 @@ STATIC mp_obj_t mod_trezorcrypto_AesGcm_finish(mp_obj_t self) {
o->state = STATE_FAILED;
mp_raise_type(&mp_type_RuntimeError);
}
+ if (n_args == 2) {
+ mp_buffer_info_t expected_tag = {0};
+ mp_get_buffer_raise(args[1], &expected_tag, MP_BUFFER_READ);
+ if (expected_tag.len != 16) {
+ mp_raise_ValueError(
+ MP_ERROR_TEXT("Invalid length of the tag. It has to be 16 bytes."));
+ }
+ if (!consteq((uint8_t *)tag.buf, tag.len, (uint8_t *)expected_tag.buf,
+ expected_tag.len)) {
+ mp_raise_msg(&mp_type_RuntimeError,
+ MP_ERROR_TEXT("Authentication failed."));
+ }
+ }
return mp_obj_new_str_from_vstr(&mp_type_bytes, &tag);
}
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_AesGcm_finish_obj,
- mod_trezorcrypto_AesGcm_finish);
+STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_AesGcm_finish_obj,
+ 1, 2,
+ mod_trezorcrypto_AesGcm_finish);
STATIC mp_obj_t mod_trezorcrypto_AesGcm___del__(mp_obj_t self) {
mp_obj_AesGcm_t *o = MP_OBJ_TO_PTR(self);
diff --git a/core/mocks/generated/trezorcrypto/__init__.pyi b/core/mocks/generated/trezorcrypto/__init__.pyi
index 03bf7fc4..a3ebdc2c 100644
--- a/core/mocks/generated/trezorcrypto/__init__.pyi
+++ b/core/mocks/generated/trezorcrypto/__init__.pyi
@@ -77,9 +77,10 @@ class aesgcm:
finish().
"""
- def finish(self) -> bytes:
+ def finish(self, expected_tag: AnyBytes | None = None) -> bytes:
"""
- Compute GCM authentication tag.
+ Compute GCM authentication tag. The `expected_tag` is required when
+ decrypting.
"""
diff --git a/core/src/trezor/wire/thp/crypto.py b/core/src/trezor/wire/thp/crypto.py
index 58da1282..039ca594 100644
--- a/core/src/trezor/wire/thp/crypto.py
+++ b/core/src/trezor/wire/thp/crypto.py
@@ -53,8 +53,11 @@ def dec(
aes_ctx = aesgcm(key, iv)
aes_ctx.auth(auth_data)
aes_ctx.decrypt_in_place(buffer)
- computed_tag = aes_ctx.finish()
- return utils.consteq(computed_tag, tag)
+ try:
+ aes_ctx.finish(tag)
+ except RuntimeError:
+ return False
+ return True
PROTOCOL_NAME = b"Noise_XX_25519_AESGCM_SHA256\x00\x00\x00\x00"
@@ -157,8 +160,9 @@ class Handshake:
host_static_public_key = memoryview(encrypted_host_static_public_key)[
:PUBKEY_LENGTH
]
- tag = aes_ctx.finish()
- if not utils.consteq(tag, encrypted_host_static_public_key[-16:]):
+ try:
+ aes_ctx.finish(encrypted_host_static_public_key[-16:])
+ except RuntimeError:
raise ThpDecryptionError()
self.ck, self.k = _hkdf(
@@ -175,8 +179,9 @@ class Handshake:
log.debug(
__name__, "th2 - dec (key: %s, nonce: %d)", hexlify_if_bytes(self.k), 0
)
- tag = aes_ctx.finish()
- if not utils.consteq(tag, encrypted_payload[-16:]):
+ try:
+ aes_ctx.finish(encrypted_payload[-16:])
+ except RuntimeError:
raise ThpDecryptionError()
self.key_receive, self.key_send = _hkdf(self.ck, b"")
Why this scored 47/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.