chore(core, crypto): introduce `AuthenticationError`
What changed, and why it matters
This commit is a code cleanup, not a security fix. It creates a new, more specific error type called AuthenticationError and changes AES-GCM and ChaCha20-Poly1305 decryption to throw that error instead of a generic RuntimeError when authentication fails. It also updates the Python code that catches those errors to catch the new type. The cryptographic behavior itself is unchanged.
No security action required. This is a refactoring commit. Reviewers may verify that all callers previously catching RuntimeError for AEAD failures have been updated, which the diff appears to cover.
Security signals we found
Changed exception type for AEAD authentication failures from RuntimeError to a dedicated AuthenticationError
Updated all Python exception handlers to catch the new specific exception type
No change to cryptographic verification logic or constant-time comparison
Evidence from the diff
The patch introduces mp_type_AuthenticationError (a MicroPython exception subclass of Exception) in the trezorcrypto module and exports it through trezor.crypto. The C implementations of AesGcm_decrypt_finish and ChaCha20Poly1305_decrypt_finish now raise AuthenticationError instead of RuntimeError on tag/MAC mismatch. Python callers in Monero chacha_poly, WebAuthn credential, and THP crypto layers update their exception handlers accordingly. Tests are updated to assert the new exception type. No cryptographic logic, constant-time comparison, or failure handling behavior changes.
Changed components
core/embed/upymod/modtrezorcrypto (AES-GCM and ChaCha20-Poly1305 C modules)core/src/trezor/crypto/__init__.pycore/src/apps/monero/xmr/chacha_poly.pycore/src/apps/webauthn/credential.pycore/src/trezor/wire/thp/crypto.pycore/tests/test_trezor.crypto.aesgcm.pycore/tests/test_trezor.crypto.chacha20poly1305.pyInspect captured patch +70 / −13
diff --git a/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-aesgcm.h b/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-aesgcm.h
index 1fc2472f..eb3bffad 100644
--- a/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-aesgcm.h
+++ b/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-aesgcm.h
@@ -23,6 +23,8 @@
#include "consteq.h"
#include "memzero.h"
+extern const mp_obj_type_t mp_type_AuthenticationError;
+
/// package: trezorcrypto.__init__
/// class aesgcm_encrypt:
@@ -273,7 +275,7 @@ STATIC mp_obj_t mod_trezorcrypto_AesGcm_decrypt_finish(mp_obj_t self,
}
if (!consteq(tag.buf, exp_tag.buf, exp_tag.len)) {
- mp_raise_msg(&mp_type_RuntimeError,
+ mp_raise_msg(&mp_type_AuthenticationError,
MP_ERROR_TEXT("Authentication failed."));
}
diff --git a/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-chacha20poly1305.h b/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-chacha20poly1305.h
index 615952d7..f22596d8 100644
--- a/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-chacha20poly1305.h
+++ b/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-chacha20poly1305.h
@@ -23,6 +23,8 @@
#include "consteq.h"
#include "memzero.h"
+extern const mp_obj_type_t mp_type_AuthenticationError;
+
/// package: trezorcrypto.__init__
/// class chacha20poly1305_encrypt:
@@ -198,7 +200,7 @@ STATIC mp_obj_t mod_trezorcrypto_ChaCha20Poly1305_decrypt_finish(
rfc7539_finish(&(o->ctx), o->alen, o->plen, (uint8_t *)mac.buf);
if (!consteq(mac.buf, exp_mac.buf, exp_mac.len)) {
- mp_raise_msg(&mp_type_RuntimeError,
+ mp_raise_msg(&mp_type_AuthenticationError,
MP_ERROR_TEXT("Authentication failed."));
}
diff --git a/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-common.h b/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-common.h
new file mode 100644
index 00000000..7e2a1c01
--- /dev/null
+++ b/core/embed/upymod/modtrezorcrypto/modtrezorcrypto-common.h
@@ -0,0 +1,25 @@
+/*
+ * 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/>.
+ */
+
+/// package: trezorcrypto.__init__
+
+/// class AuthenticationError(Exception):
+/// """
+/// Authentication failed.
+/// """
diff --git a/core/embed/upymod/modtrezorcrypto/modtrezorcrypto.c b/core/embed/upymod/modtrezorcrypto/modtrezorcrypto.c
index 43c00564..15d1c214 100644
--- a/core/embed/upymod/modtrezorcrypto/modtrezorcrypto.c
+++ b/core/embed/upymod/modtrezorcrypto/modtrezorcrypto.c
@@ -81,8 +81,12 @@ static void wrapped_ui_wait_callback(uint32_t current, uint32_t total) {
#include "modtrezorcrypto-nem.h"
#endif
+MP_DEFINE_EXCEPTION(AuthenticationError, Exception)
+
STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorcrypto)},
+ {MP_ROM_QSTR(MP_QSTR_AuthenticationError),
+ MP_ROM_PTR(&mp_type_AuthenticationError)},
{MP_ROM_QSTR(MP_QSTR_aes), MP_ROM_PTR(&mod_trezorcrypto_AES_type)},
#if USE_AES_GCM
{MP_ROM_QSTR(MP_QSTR_aesgcm_decrypt),
diff --git a/core/mocks/generated/trezorcrypto/__init__.pyi b/core/mocks/generated/trezorcrypto/__init__.pyi
index f18e4039..4d408118 100644
--- a/core/mocks/generated/trezorcrypto/__init__.pyi
+++ b/core/mocks/generated/trezorcrypto/__init__.pyi
@@ -258,6 +258,13 @@ class chacha20poly1305_decrypt:
"""
+# upymod/modtrezorcrypto/modtrezorcrypto-common.h
+class AuthenticationError(Exception):
+ """
+ Authentication failed.
+ """
+
+
# upymod/modtrezorcrypto/modtrezorcrypto-groestl.h
class groestl512:
"""
diff --git a/core/src/apps/monero/xmr/chacha_poly.py b/core/src/apps/monero/xmr/chacha_poly.py
index b36e69ce..eb93e333 100644
--- a/core/src/apps/monero/xmr/chacha_poly.py
+++ b/core/src/apps/monero/xmr/chacha_poly.py
@@ -1,4 +1,8 @@
-from trezor.crypto import chacha20poly1305_decrypt, chacha20poly1305_encrypt
+from trezor.crypto import (
+ AuthenticationError,
+ chacha20poly1305_decrypt,
+ chacha20poly1305_encrypt,
+)
def encrypt(key: bytes, plaintext: bytes, associated_data: bytes | None = None):
@@ -33,7 +37,7 @@ def _decrypt(
plaintext = cipher.decrypt(ciphertext)
try:
cipher.finish(exp_tag)
- except RuntimeError:
+ except AuthenticationError:
raise ValueError("tag invalid")
return plaintext
diff --git a/core/src/apps/webauthn/credential.py b/core/src/apps/webauthn/credential.py
index 44571c1c..355ce7ed 100644
--- a/core/src/apps/webauthn/credential.py
+++ b/core/src/apps/webauthn/credential.py
@@ -6,6 +6,7 @@ from ubinascii import hexlify
import storage.device as storage_device
from trezor import utils
from trezor.crypto import (
+ AuthenticationError,
chacha20poly1305_decrypt,
chacha20poly1305_encrypt,
der,
@@ -213,7 +214,7 @@ class Fido2Credential(Credential):
data = ctx.decrypt(ciphertext)
try:
ctx.finish(tag)
- except RuntimeError:
+ except AuthenticationError:
raise ValueError # inauthentic ciphertext
try:
diff --git a/core/src/trezor/crypto/__init__.py b/core/src/trezor/crypto/__init__.py
index ee3fbc7d..bbedbe1a 100644
--- a/core/src/trezor/crypto/__init__.py
+++ b/core/src/trezor/crypto/__init__.py
@@ -1,4 +1,5 @@
from trezorcrypto import ( # noqa: F401
+ AuthenticationError,
aes,
bip32,
bip39,
diff --git a/core/src/trezor/wire/thp/crypto.py b/core/src/trezor/wire/thp/crypto.py
index 532d5e77..72572e59 100644
--- a/core/src/trezor/wire/thp/crypto.py
+++ b/core/src/trezor/wire/thp/crypto.py
@@ -1,6 +1,13 @@
import ustruct
from micropython import const
-from trezorcrypto import aesgcm_decrypt, aesgcm_encrypt, bip32, curve25519, hmac
+from trezorcrypto import (
+ AuthenticationError,
+ aesgcm_decrypt,
+ aesgcm_encrypt,
+ bip32,
+ curve25519,
+ hmac,
+)
from typing import TYPE_CHECKING
from storage import device
@@ -55,7 +62,7 @@ def dec(
aes_ctx.decrypt_in_place(buffer)
try:
aes_ctx.finish(tag)
- except RuntimeError:
+ except AuthenticationError:
return False
return True
@@ -162,7 +169,7 @@ class Handshake:
]
try:
aes_ctx.finish(encrypted_host_static_public_key[-16:])
- except RuntimeError:
+ except AuthenticationError:
raise ThpDecryptionError()
self.ck, self.k = _hkdf(
@@ -181,7 +188,7 @@ class Handshake:
)
try:
aes_ctx.finish(encrypted_payload[-16:])
- except RuntimeError:
+ except AuthenticationError:
raise ThpDecryptionError()
self.key_receive, self.key_send = _hkdf(self.ck, b"")
diff --git a/core/tests/test_trezor.crypto.aesgcm.py b/core/tests/test_trezor.crypto.aesgcm.py
index 68eea549..55a2f32d 100644
--- a/core/tests/test_trezor.crypto.aesgcm.py
+++ b/core/tests/test_trezor.crypto.aesgcm.py
@@ -1,7 +1,7 @@
# flake8: noqa: F403,F405
from common import * # isort:skip
-from trezor.crypto import aesgcm_decrypt, aesgcm_encrypt
+from trezor.crypto import AuthenticationError, aesgcm_decrypt, aesgcm_encrypt
class TestCryptoAes(unittest.TestCase):
@@ -171,7 +171,7 @@ class TestCryptoAes(unittest.TestCase):
self.assertEqual(ctx.decrypt(ct), pt)
# Try finishing the decryption with an invalid tag
- with self.assertRaises(RuntimeError) as e:
+ with self.assertRaises(AuthenticationError) as e:
ctx.finish(invalid_tag)
self.assertEqual(
e.value.value,
diff --git a/core/tests/test_trezor.crypto.chacha20poly1305.py b/core/tests/test_trezor.crypto.chacha20poly1305.py
index 4ae22ed8..1b42846c 100644
--- a/core/tests/test_trezor.crypto.chacha20poly1305.py
+++ b/core/tests/test_trezor.crypto.chacha20poly1305.py
@@ -1,7 +1,11 @@
# flake8: noqa: F403,F405
from common import * # isort:skip
-from trezor.crypto import chacha20poly1305_decrypt, chacha20poly1305_encrypt
+from trezor.crypto import (
+ AuthenticationError,
+ chacha20poly1305_decrypt,
+ chacha20poly1305_encrypt,
+)
class TestCryptoChaCha20Poly1305(unittest.TestCase):
@@ -92,7 +96,7 @@ class TestCryptoChaCha20Poly1305(unittest.TestCase):
ctx = chacha20poly1305_decrypt(key, nonce)
ctx.auth(aad)
ctx.decrypt(ciphertext)
- with self.assertRaises(RuntimeError) as e:
+ with self.assertRaises(AuthenticationError) as e:
ctx.finish(invalid_mac)
self.assertEqual(e.value.value, "Authentication failed.")
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.