diff --git a/core/src/apps/webauthn/credential.py b/core/src/apps/webauthn/credential.py index 2530e44f1..0cd4eca94 100644 --- a/core/src/apps/webauthn/credential.py +++ b/core/src/apps/webauthn/credential.py @@ -18,6 +18,10 @@ _CRED_ID_VERSION = b"\xf1\xd0\x02\x00" _CRED_ID_MIN_LENGTH = const(33) _KEY_HANDLE_LENGTH = const(64) +# Maximum supported length of the RP name, user name or user displayName in bytes. +# Note: The WebAuthn spec allows authenticators to truncate to 64 bytes or more. +NAME_MAX_LENGTH = const(100) + # Credential ID keys _CRED_ID_RP_ID = const(1) _CRED_ID_RP_NAME = const(2) @@ -208,6 +212,18 @@ class Fido2Credential(Credential): return cred + def truncate_names(self) -> None: + if self.rp_name: + self.rp_name = utils.truncate_utf8(self.rp_name, NAME_MAX_LENGTH) + + if self.user_name: + self.user_name = utils.truncate_utf8(self.user_name, NAME_MAX_LENGTH) + + if self.user_display_name: + self.user_display_name = utils.truncate_utf8( + self.user_display_name, NAME_MAX_LENGTH + ) + def check_required_fields(self) -> bool: return ( self.rp_id is not None diff --git a/core/src/apps/webauthn/fido2.py b/core/src/apps/webauthn/fido2.py index 18b68da3d..0a5937d35 100644 --- a/core/src/apps/webauthn/fido2.py +++ b/core/src/apps/webauthn/fido2.py @@ -1355,6 +1355,7 @@ def cbor_make_credential(req: Cmd, dialog_mgr: DialogManager) -> Optional[Cmd]: cred.user_id = user["id"] cred.user_name = user.get("name", None) cred.user_display_name = user.get("displayName", None) + cred.truncate_names() # Check if any of the credential descriptors in the exclude list belong to this authenticator. exclude_list = param.get(_MAKECRED_CMD_EXCLUDE_LIST, []) diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py index 587efdbfa..04e683e9e 100644 --- a/core/src/trezor/utils.py +++ b/core/src/trezor/utils.py @@ -138,3 +138,17 @@ def obj_repr(o: object) -> str: else: d = o.__dict__ return "<%s: %s>" % (o.__class__.__name__, d) + + +def truncate_utf8(string: str, max_bytes: int) -> str: + """Truncate the codepoints of a string so that its UTF-8 encoding is at most `max_bytes` in length.""" + data = string.encode() + if len(data) <= max_bytes: + return string + + # Find the starting position of the last codepoint in data[0 : max_bytes + 1]. + i = max_bytes + while i >= 0 and data[i] & 0xC0 == 0x80: + i -= 1 + + return data[:i].decode() diff --git a/core/tests/test_apps.webauthn.credential.py b/core/tests/test_apps.webauthn.credential.py index 84d63c834..a2fb008f1 100644 --- a/core/tests/test_apps.webauthn.credential.py +++ b/core/tests/test_apps.webauthn.credential.py @@ -1,7 +1,7 @@ from common import * import storage from apps.common import mnemonic -from apps.webauthn.credential import Fido2Credential +from apps.webauthn.credential import Fido2Credential, NAME_MAX_LENGTH from trezor.crypto.curve import nist256p1 from trezor.crypto.hashlib import sha256 @@ -59,5 +59,20 @@ class TestCredential(unittest.TestCase): self.assertEqual(hexlify(cred.hmac_secret_key()), cred_random) self.assertEqual(hexlify(cred.public_key()), public_key) + def test_truncation(self): + cred = Fido2Credential() + cred.truncate_names() + self.assertIsNone(cred.rp_name) + self.assertIsNone(cred.user_name) + self.assertIsNone(cred.user_display_name) + + cred.rp_name = "a" * (NAME_MAX_LENGTH - 2) + "\u0123" + cred.user_name = "a" * (NAME_MAX_LENGTH - 1) + "\u0123" + cred.user_display_name = "a" * NAME_MAX_LENGTH + "\u0123" + cred.truncate_names() + self.assertEqual(cred.rp_name, "a" * (NAME_MAX_LENGTH - 2) + "\u0123") + self.assertEqual(cred.user_name, "a" * (NAME_MAX_LENGTH - 1)) + self.assertEqual(cred.user_display_name, "a" * NAME_MAX_LENGTH) + if __name__ == '__main__': unittest.main() diff --git a/core/tests/test_trezor.utils.py b/core/tests/test_trezor.utils.py index 0adba98f4..21427451b 100644 --- a/core/tests/test_trezor.utils.py +++ b/core/tests/test_trezor.utils.py @@ -13,6 +13,28 @@ class TestUtils(unittest.TestCase): self.assertEqual(c[i].stop, 100 if (i == 14) else (i + 1) * 7) self.assertEqual(c[i].step, 1) + def test_truncate_utf8(self): + self.assertEqual(utils.truncate_utf8("", 3), "") + self.assertEqual(utils.truncate_utf8("a", 3), "a") + self.assertEqual(utils.truncate_utf8("ab", 3), "ab") + self.assertEqual(utils.truncate_utf8("abc", 3), "abc") + self.assertEqual(utils.truncate_utf8("abcd", 3), "abc") + self.assertEqual(utils.truncate_utf8("abcde", 3), "abc") + self.assertEqual(utils.truncate_utf8("a\u0123", 3), "a\u0123") # b'a\xc4\xa3' + self.assertEqual(utils.truncate_utf8("a\u1234", 3), "a") # b'a\xe1\x88\xb4' + self.assertEqual(utils.truncate_utf8("ab\u0123", 3), "ab") # b'ab\xc4\xa3' + self.assertEqual(utils.truncate_utf8("ab\u1234", 3), "ab") # b'ab\xe1\x88\xb4' + self.assertEqual(utils.truncate_utf8("abc\u0123", 3), "abc") # b'abc\xc4\xa3' + self.assertEqual(utils.truncate_utf8("abc\u1234", 3), "abc") # b'abc\xe1\x88\xb4' + self.assertEqual(utils.truncate_utf8("\u1234\u5678", 0), "") # b'\xe1\x88\xb4\xe5\x99\xb8 + self.assertEqual(utils.truncate_utf8("\u1234\u5678", 1), "") # b'\xe1\x88\xb4\xe5\x99\xb8 + self.assertEqual(utils.truncate_utf8("\u1234\u5678", 2), "") # b'\xe1\x88\xb4\xe5\x99\xb8 + self.assertEqual(utils.truncate_utf8("\u1234\u5678", 3), "\u1234") # b'\xe1\x88\xb4\xe5\x99\xb8 + self.assertEqual(utils.truncate_utf8("\u1234\u5678", 4), "\u1234") # b'\xe1\x88\xb4\xe5\x99\xb8 + self.assertEqual(utils.truncate_utf8("\u1234\u5678", 5), "\u1234") # b'\xe1\x88\xb4\xe5\x99\xb8 + self.assertEqual(utils.truncate_utf8("\u1234\u5678", 6), "\u1234\u5678") # b'\xe1\x88\xb4\xe5\x99\xb8 + self.assertEqual(utils.truncate_utf8("\u1234\u5678", 7), "\u1234\u5678") # b'\xe1\x88\xb4\xe5\x99\xb8 + if __name__ == '__main__': unittest.main()