chore: add script to provision Tropic model
What changed, and why it matters
This commit adds a build/test helper script and sample configuration files for simulating a Tropic security chip during automated testing. It does not change any firmware code that runs on real Trezor devices, and the private keys it includes are explicitly test-only artifacts copied from a public vendor example. There is no indication this is a security fix or that it introduces a vulnerability.
No security action required. If desired, verify that the committed PEM files match the upstream ts-tvl example_config and that CI uses these files only in test/simulator contexts, not in production firmware builds.
Security signals we found
Private key material present in repository, but clearly labeled as test-only example keys for a simulator
No changes to on-device firmware or production code paths
No changelog entry and commit title marked as chore
No vendor or researcher security disclosure references present
Evidence from the diff
The commit introduces core/tools/generate_tropic_model_config.py, which generates a YAML configuration for the ts-tvl Tropic model simulator used in Trezor’s test suite. It also adds tests/tropic_model/config.yml and three PEM files (certificate, private key, public key) copied from the vendor’s example_config. The script performs Ed25519 private-key clamping and builds a certificate chain for Trezor’s device-authenticity self-test flow. The Makefile is updated to include the new generator in gen/gen_check targets. No runtime firmware code is modified.
Changed components
tests/tropic_model/config.ymltests/tropic_model/tropic01_ese_certificate_1.pemtests/tropic_model/tropic01_ese_private_key_1.pemtests/tropic_model/tropic01_ese_public_key_1.pemcore/tools/generate_tropic_model_config.pyMakefileInspect captured patch +255 / −2
diff --git a/Makefile b/Makefile
index 279c5c906..c1b7cbe5d 100644
--- a/Makefile
+++ b/Makefile
@@ -163,6 +163,12 @@ lsgen: ## generate linker scripts
lsgen_check: ## check generated linker scripts
lsgen --check
-gen: templates mocks icons protobuf vendorheader solana_templates bootloader_hashes lsgen ## regenerate auto-generated files from sources
+tropic_model_config:
+ ./core/tools/generate_tropic_model_config.py
-gen_check: templates_check mocks_check icons_check protobuf_check vendorheader_check solana_templates_check bootloader_hashes_check lsgen_check ## check validity of auto-generated files
+tropic_model_config_check:
+ ./core/tools/generate_tropic_model_config.py --check
+
+gen: templates mocks icons protobuf vendorheader solana_templates bootloader_hashes lsgen tropic_model_config ## regenerate auto-generated files from sources
+
+gen_check: templates_check mocks_check icons_check protobuf_check vendorheader_check solana_templates_check bootloader_hashes_check lsgen_check tropic_model_config_check ## check validity of auto-generated files
diff --git a/core/tools/generate_tropic_model_config.py b/core/tools/generate_tropic_model_config.py
new file mode 100755
index 000000000..0e3ef482f
--- /dev/null
+++ b/core/tools/generate_tropic_model_config.py
@@ -0,0 +1,160 @@
+#!/usr/bin/env python3
+
+import hashlib
+import os
+from pathlib import Path
+
+import click
+import yaml
+from cryptography import x509
+from cryptography.hazmat.primitives import serialization
+
+HERE = Path(__file__).parent
+ROOT = HERE.parent.parent.resolve()
+CONFIG_DIR = ROOT / "tests" / "tropic_model"
+DEST_PATH = CONFIG_DIR / "config.yml"
+
+# private key used by the Tropic model to sign
+TROPIC_KEY = CONFIG_DIR / "tropic_key.pem"
+
+# certificate of the Tropic model - signed by the root authority
+TROPIC_CERT = CONFIG_DIR / "tropic_cert.pem"
+
+# certificate of the root authority
+ROOT_CERT = CONFIG_DIR / "root_cert.pem"
+
+VENDOR_CONFIG_DIR = ROOT / "vendor" / "ts-tvl" / "model_configs" / "example_config"
+EXTRA_FILES = [
+ VENDOR_CONFIG_DIR / "tropic01_ese_certificate_1.pem",
+ VENDOR_CONFIG_DIR / "tropic01_ese_private_key_1.pem",
+ VENDOR_CONFIG_DIR / "tropic01_ese_public_key_1.pem",
+]
+
+
+@click.command()
+@click.option("--check", is_flag=True)
+def generate_config(check):
+ tropic_key = serialization.load_pem_private_key(
+ TROPIC_KEY.read_bytes(), password=None
+ )
+
+ tropic_a = tropic_key.public_key().public_bytes(
+ encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
+ )
+
+ tropic_private_key_bytes = tropic_key.private_bytes(
+ encoding=serialization.Encoding.Raw,
+ format=serialization.PrivateFormat.Raw,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
+
+ # perform clamping
+ # https://www.jcraige.com/an-explainer-on-ed25519-clamping
+ h = hashlib.sha512(tropic_private_key_bytes).digest()
+ tropic_s = bytearray(h[:32])
+ tropic_s[0] &= 248
+ tropic_s[31] &= 63
+ tropic_s[31] |= 64
+ tropic_s = bytes(tropic_s)
+
+ tropic_prefix = hashlib.sha512(tropic_s).digest()[:32]
+
+ tropic_cert = x509.load_pem_x509_certificate(TROPIC_CERT.read_bytes())
+ tropic_cert_der_bytes = tropic_cert.public_bytes(serialization.Encoding.DER)
+
+ root_cert = x509.load_pem_x509_certificate(ROOT_CERT.read_bytes())
+ root_cert_der_bytes = root_cert.public_bytes(serialization.Encoding.DER)
+
+ # certificate chain with the length prefix
+ all_cert_bytes = (
+ (len(tropic_cert_der_bytes) + len(root_cert_der_bytes)).to_bytes(2)
+ + tropic_cert_der_bytes
+ + root_cert_der_bytes
+ )
+
+ SLOT_LEN = 444
+
+ # make sure they fit in 3 slots, which is what we have available
+ assert len(all_cert_bytes) < SLOT_LEN * 3
+
+ # split the data in 3 slots
+ slot_1_bytes = all_cert_bytes[:SLOT_LEN]
+ slot_2_bytes = all_cert_bytes[SLOT_LEN : SLOT_LEN * 2]
+ slot_3_bytes = all_cert_bytes[SLOT_LEN * 2 : SLOT_LEN * 3]
+
+ # save the data starting at slot 3
+ # see https://github.com/trezor/trezor-firmware/blob/main/core/embed/sec/tropic/inc/sec/tropic.h#L31
+ TROPIC_DEVICE_CERT_FIRST_SLOT = 3
+ user_data = {}
+ for i, data in enumerate([slot_1_bytes, slot_2_bytes, slot_3_bytes]):
+ if len(data) != 0:
+ if len(data) < SLOT_LEN: # pad last slot
+ data += b"\x00" * (SLOT_LEN - len(data))
+ user_data[TROPIC_DEVICE_CERT_FIRST_SLOT + i] = {"value": data}
+
+ config_dict = {
+ "s_t_priv": "tropic01_ese_private_key_1.pem",
+ "s_t_pub": "tropic01_ese_public_key_1.pem",
+ "x509_certificate": "tropic01_ese_certificate_1.pem",
+ "debug_random_value": b"\x00\xc0\xff\xee",
+ "r_user_data": user_data, # certificate chain
+ "r_ecc_keys": { # signing key at index 0
+ 0: {
+ "a": tropic_a,
+ "s": tropic_s,
+ "prefix": tropic_prefix,
+ "origin": 2, # imported key
+ }
+ },
+ }
+
+ config = yaml.dump(config_dict)
+
+ if check:
+ if not DEST_PATH.exists():
+ print(f"{DEST_PATH} missing")
+ raise click.ClickException("Config file is missing")
+ elif config != DEST_PATH.read_text():
+ print(f"{DEST_PATH} is out of date")
+ raise click.ClickException("Config file is out of date")
+ for extra_file in EXTRA_FILES:
+ extra_file_dest = CONFIG_DIR / extra_file.name
+ if not extra_file_dest.exists():
+ print(f"{extra_file_dest} missing")
+ raise click.ClickException("Extra config file missing")
+ elif extra_file.read_bytes() != extra_file_dest.read_bytes():
+ print(f"{extra_file_dest} is out of date")
+ raise click.ClickException("Extra config file is out of date")
+ else:
+ tropic_key_stat = TROPIC_KEY.stat()
+ tropic_cert_stat = TROPIC_CERT.stat()
+ root_cert_stat = ROOT_CERT.stat()
+ DEST_PATH.write_text(config)
+ os.utime(
+ DEST_PATH,
+ ns=(
+ max(
+ tropic_key_stat.st_atime_ns,
+ tropic_cert_stat.st_atime_ns,
+ root_cert_stat.st_atime_ns,
+ ),
+ max(
+ tropic_key_stat.st_mtime_ns,
+ tropic_cert_stat.st_mtime_ns,
+ root_cert_stat.st_mtime_ns,
+ ),
+ ),
+ )
+
+ for extra_file in EXTRA_FILES:
+ extra_file_dest = CONFIG_DIR / extra_file.name
+ extra_file_dest.write_bytes(extra_file.read_bytes())
+ extra_file_stat = extra_file.stat()
+ os.utime(
+ extra_file_dest,
+ ns=(extra_file_stat.st_atime_ns, extra_file_stat.st_mtime_ns),
+ )
+
+
+if __name__ == "__main__":
+ generate_config()
diff --git a/tests/tropic_model/README.md b/tests/tropic_model/README.md
new file mode 100644
index 000000000..f5c20cceb
--- /dev/null
+++ b/tests/tropic_model/README.md
@@ -0,0 +1,35 @@
+This directory contains files needed to launch the "Tropic model" - [ts-tvl](https://github.com/tropicsquare/ts-tvl/) - which is used to simulate the presence of the TROPIC01 chip during tests.
+
+The Tropic model requires:
+ * (I) a keypair and a certificate it will use to communicate to the host (`tropic01_ese_*`) - these are simply copied from [example_config](https://github.com/tropicsquare/ts-tvl/tree/master/model_configs/example_config)
+ * (II) a certificate chain and a keypair that will be used during Trezor's authenticity check
+
+The root key, certificate chain and keypair are generated using these commands:
+
+```
+# generate root keypair
+openssl genpkey -algorithm Ed25519 -out root_key.pem
+openssl pkey -in root_key.pem -pubout -out root_pubkey.pem
+
+# see the root pubkey - to be used in test
+openssl pkey -in root_pubkey.pem -pubin -outform DER | tail -c 32 | xxd -p -c 256
+
+# generate root certificate (signed by the root key)
+openssl req -new -x509 -key root_key.pem -out root_cert.pem -days 36500
+
+# generate device key pair
+openssl genpkey -algorithm Ed25519 -out tropic_key.pem
+openssl pkey -in tropic_key.pem -pubout -out tropic_pubkey.pem
+
+# generate certificate signing request
+openssl req -new -key tropic_key.pem -out tropic.csr -subj "/CN=T3W1"
+
+# use the signing request to generate a device certificate signed by the authority
+openssl x509 -req -in tropic.csr -CA root_cert.pem -CAkey root_key.pem -CAcreateserial -out tropic_cert.pem -days 36500
+```
+
+`ts-tvl` then uses a YAML config file to load the above keys and certificates.
+ * (I) go into: `s_t_priv`, `s_t_pub` and `x509_certificate` as required by [`ts-tvl`](https://github.com/tropicsquare/ts-tvl/blob/master/model_configs/example_config/example_config.yml)
+ * (II) go into: `r_ecc_keys` and `r_user_data` as required by [Trezor's authenticity check](https://github.com/trezor/trezor-firmware/blob/main/core/src/apps/management/authenticate_device.py)
+
+The config file itself is generated using `core/tools/generate_tropic_model_config.py`.
diff --git a/tests/tropic_model/config.yml b/tests/tropic_model/config.yml
new file mode 100644
index 000000000..ee773694e
--- /dev/null
+++ b/tests/tropic_model/config.yml
@@ -0,0 +1,35 @@
+debug_random_value: !!binary |
+ AMD/7g==
+r_ecc_keys:
+ 0:
+ a: !!binary |
+ /znnOD2vDW1Znx2ymKbAur+8R+x3vj8uL/e5VUqnH38=
+ origin: 2
+ prefix: !!binary |
+ a2BLVJLG4DGBZBPl5ipLvlLOj8zovqvCgejgJtWKGw8=
+ s: !!binary |
+ 2JitRIPXXNvDPBcYOyKAWEEGn1k97MfNKx+z4/kNQEk=
+r_user_data:
+ 3:
+ value: !!binary |
+ AhwwgeUwgZgCFAYbogFv5WvsoMxMwanH3uF4NejHMAUGAytlcDANMQswCQYDVQQGEwJDWjAgFw0y
+ NTEwMDExNTU5MjBaGA8yMTI1MDkwNzE1NTkyMFowHDELMAkGA1UEBhMCQ1oxDTALBgNVBAMMBFQz
+ VzEwKjAFBgMrZXADIQD/Oec4Pa8NbVmfHbKYpsC6v7xH7He+Py4v97lVSqcffzAFBgMrZXADQQC/
+ Sg/OI9Oa4IcjAGkqswmsZY782uzrHqqOdb8C9QHMQEJFKggXH39rxtXRU2kmj+ryTkYNA4vUhIKm
+ +zvKUGkEMIIBMDCB46ADAgECAhRMR5WXXWH8xNquZfZtCCchQO757TAFBgMrZXAwDTELMAkGA1UE
+ BhMCQ1owIBcNMjUxMDAxMTUzMzA2WhgPMjEyNTA5MDcxNTMzMDZaMA0xCzAJBgNVBAYTAkNaMCow
+ BQYDK2VwAyEAGrHF8S9FcODeXBao2f7qOB9TyNgT/usOsvt/OT8ra1+jUzBRMB0GA1UdDgQWBBSY
+ DtrUTbT8JI4eHR/OIHl787VZ1zAfBgNVHSMEGDAWgBSYDtrUTbT8JI4eHR/O
+ 4:
+ value: !!binary |
+ IHl787VZ1zAPBgNVHRMBAf8EBTADAQH/MAUGAytlcANBAD2wmHseXqtUk2QJuwRUpdoestUlhKGR
+ xrD1yhY8fxCvCzU+b4qZffXCyyk4qgDiTgdNHMz04zL6nKcNhT+geAQAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+s_t_priv: tropic01_ese_private_key_1.pem
+s_t_pub: tropic01_ese_public_key_1.pem
+x509_certificate: tropic01_ese_certificate_1.pem
diff --git a/tests/tropic_model/tropic01_ese_certificate_1.pem b/tests/tropic_model/tropic01_ese_certificate_1.pem
new file mode 100644
index 000000000..9d2f4bba2
--- /dev/null
+++ b/tests/tropic_model/tropic01_ese_certificate_1.pem
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBkDCCARegAwIBAgIBATAKBggqhkjOPQQDAzBZMQswCQYDVQQGEwJDWjEPMA0G
+A1UEBwwGUHJhZ3VlMRYwFAYDVQQKDA1Ucm9waWMgU3F1YXJlMSEwHwYDVQQDDBhU
+cm9waWMgU3F1YXJlIFJvb3QgQ0EgdjEwHhcNMjQwNjA0MDc0MDMxWhcNMjUwNjA0
+MDc0MDMxWjAXMRUwEwYDVQQDDAxUUk9QSUMwMSBlU0UwKjAFBgMrZW4DIQAx6Qrx
+UEUQ7k79eRMzQUgViaKJXMX7sT7VcRwem4GYcqNBMD8wDAYDVR0TAQH/BAIwADAO
+BgNVHQ8BAf8EBAMCAwgwHwYDVR0jBBgwFoAUipzvY3hE2wO8KSaA0eZVB9Xej0cw
+CgYIKoZIzj0EAwMDZwAwZAIwDLRviNFPbY+5UWmuEBY2/3aSuTQf9iKXUZmrDh9o
+3mZS13juBhHV58yAg0vbqzD1AjBRNE490wx/su6TSuGMj9ZX37WugegiBI/r3TQo
+eL/T/JgO+JReqwIJopvpb2Trhg4=
+-----END CERTIFICATE-----
diff --git a/tests/tropic_model/tropic01_ese_private_key_1.pem b/tests/tropic_model/tropic01_ese_private_key_1.pem
new file mode 100644
index 000000000..2b0b1e72d
--- /dev/null
+++ b/tests/tropic_model/tropic01_ese_private_key_1.pem
@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VuBCIEILBCgH/pL/h130UnD21VqirffNeDvlZaF9Rm7tVFvuNn
+-----END PRIVATE KEY-----
diff --git a/tests/tropic_model/tropic01_ese_public_key_1.pem b/tests/tropic_model/tropic01_ese_public_key_1.pem
new file mode 100644
index 000000000..4b3718725
--- /dev/null
+++ b/tests/tropic_model/tropic01_ese_public_key_1.pem
@@ -0,0 +1,3 @@
+-----BEGIN PUBLIC KEY-----
+MCowBQYDK2VuAyEAMekK8VBFEO5O/XkTM0FIFYmiiVzF+7E+1XEcHpuBmHI=
+-----END PUBLIC KEY-----
Why this scored 13/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.