fix(python): improve handling for CA blacklists/whitelists
What changed, and why it matters
This commit improves how Trezor's Python library handles the list of trusted certificate authorities used to verify a device is genuine. Previously, if the online allowlist could not be downloaded, the tool might fail in an uncontrolled way. The change adds proper error handling and renames 'whitelist' to the more general 'allowlist' to support both approved and revoked (blacklisted) public keys. It also renames the user-facing option from --skip-whitelist to --offline. There is no direct evidence in the commit that this fixes an active security vulnerability, but it hardens the device-authentication process.
Treat as a defensive hardening improvement. Review the new AllowList parsing to ensure malformed JSON or unexpected fields cannot bypass checks, and confirm the online allowlist endpoint serves both ca_pubkeys and revoked_pubkeys correctly before deploying the T3W1 blacklist.
Security signals we found
Hardening of device authenticity verification
Addition of network-download error handling for trust anchor list
Preparation for certificate revocation list (blacklist) support
Renaming of CLI option and internal terminology from whitelist to allowlist
Evidence from the diff
The patch refactors certificate-public-key checking in trezorlib. It introduces an AllowList class that can represent either a whitelist (ca_pubkeys) or a blacklist (revoked_pubkeys). The verify_authentication_response and authenticate_device functions now accept an AllowList object instead of a raw collection of bytes. The CLI’s authenticate command now downloads the JSON allowlist, checks the HTTP status with raise_for_status(), and surfaces a clear ClickException if download or parsing fails, suggesting –offline. The –skip-whitelist flag is renamed –offline. The change also prepares support for a future blacklist for the T3W1 model.
Changed components
python/src/trezorlib/authentication.pypython/src/trezorlib/cli/device.pyInspect captured patch +50 / −22
diff --git a/python/src/trezorlib/authentication.py b/python/src/trezorlib/authentication.py
index 02991975..c57e2d7e 100644
--- a/python/src/trezorlib/authentication.py
+++ b/python/src/trezorlib/authentication.py
@@ -59,6 +59,27 @@ class DeviceNotAuthentic(Exception):
pass
+class AllowList:
+ def __init__(self, data: dict[str, t.Any]) -> None:
+ self.whitelist = None
+ self.blacklist = None
+ if "ca_pubkeys" in data:
+ self.whitelist = [bytes.fromhex(pk) for pk in data["ca_pubkeys"]]
+ if "revoked_pubkeys" in data:
+ self.blacklist = [bytes.fromhex(pk) for pk in data["revoked_pubkeys"]]
+ if self.whitelist is None and self.blacklist is None:
+ raise ValueError(
+ "Invalid allow list: no CA public keys or revoked public keys."
+ )
+
+ def is_allowed(self, pubkey: bytes) -> bool:
+ if self.whitelist is not None:
+ return pubkey in self.whitelist
+ if self.blacklist is not None:
+ return pubkey not in self.blacklist
+ raise RuntimeError("Invalid allow list: no whitelist or blacklist entries.")
+
+
class PublicKey:
@staticmethod
def from_bytes_and_oid(data: bytes, oid: ObjectIdentifier) -> PublicKey:
@@ -395,7 +416,7 @@ def verify_authentication_response(
signature: bytes,
cert_chain: t.Iterable[bytes],
*,
- whitelist: t.Collection[bytes] | None,
+ allowlist: AllowList | None,
allow_development_devices: bool = False,
root_pubkey: bytes | PublicKey | None = None,
) -> RootCertificate | None:
@@ -443,11 +464,11 @@ def verify_authentication_response(
failed = True
continue
- if whitelist is None:
- LOG.warning("Skipping public key whitelist check.")
+ if allowlist is None:
+ LOG.warning("Skipping public key allowlist check.")
else:
- if ca_cert.public_key.to_bytes() not in whitelist:
- LOG.error(f"CA certificate #{i} not in whitelist: %s", ca_cert)
+ if not allowlist.is_allowed(ca_cert.public_key.to_bytes()):
+ LOG.error(f"CA certificate #{i} denied by allowlist: %s", ca_cert)
failed = True
if not cert.is_issued_by(ca_cert, i - 1):
@@ -513,7 +534,7 @@ def authenticate_device(
session: Session,
challenge: bytes | None = None,
*,
- whitelist: t.Collection[bytes] | None = None,
+ allowlist: AllowList | None = None,
allow_development_devices: bool = False,
p256_root_pubkey: bytes | PublicKey | None = None,
ed25519_root_pubkey: bytes | PublicKey | None = None,
@@ -527,7 +548,7 @@ def authenticate_device(
challenge,
resp.optiga_signature,
resp.optiga_certificates,
- whitelist=whitelist,
+ allowlist=allowlist,
allow_development_devices=allow_development_devices,
root_pubkey=p256_root_pubkey,
)
@@ -537,7 +558,7 @@ def authenticate_device(
challenge,
resp.tropic_signature,
resp.tropic_certificates,
- whitelist=whitelist,
+ allowlist=allowlist,
allow_development_devices=allow_development_devices,
root_pubkey=ed25519_root_pubkey,
)
diff --git a/python/src/trezorlib/cli/device.py b/python/src/trezorlib/cli/device.py
index 5661cf59..ed0cbb39 100644
--- a/python/src/trezorlib/cli/device.py
+++ b/python/src/trezorlib/cli/device.py
@@ -361,7 +361,7 @@ def set_busy(session: "Session", enable: bool | None, expiry: int | None) -> Non
device.set_busy(session, expiry * 1000)
-PUBKEY_WHITELIST_URL_TEMPLATE = (
+PUBKEY_ALLOWLIST_URL_TEMPLATE = (
"https://data.trezor.io/firmware/{model}/authenticity.json"
)
@@ -386,9 +386,9 @@ def _print_auth_data(signature: bytes, certificates: t.Sequence[bytes]) -> None:
)
@click.option(
"-s",
- "--skip-whitelist",
+ "--offline",
is_flag=True,
- help="Do not check intermediate certificates against the whitelist.",
+ help="Do not check intermediate certificates against the online whitelist/CRL.",
)
@with_session(seedless=True)
def authenticate(
@@ -397,16 +397,16 @@ def authenticate(
p256_root: t.BinaryIO | None,
ed25519_root: t.BinaryIO | None,
raw: bool | None,
- skip_whitelist: bool | None,
+ offline: bool | None,
) -> None:
"""Verify the authenticity of the device.
Use the --raw option to get the raw challenge, signature, and certificate data.
Otherwise, trezorctl will attempt to decode the signatures and check their
- authenticity. By default, it will also check the public keys against a whitelist
- downloaded from Trezor servers. You can skip this check with the --skip-whitelist
- option.
+ authenticity. By default, it will also check the public keys against a
+ whitelist or CRL downloaded from Trezor servers. You can skip this check
+ with the --offline option.
"""
if hex_challenge is None:
hex_challenge = secrets.token_hex(32)
@@ -452,15 +452,22 @@ def authenticate(
authentication.LOG.addHandler(handler)
authentication.LOG.setLevel(logging.DEBUG)
- if skip_whitelist:
- whitelist = None
+ if offline:
+ allowlist = None
else:
- whitelist_json = requests.get(
- PUBKEY_WHITELIST_URL_TEMPLATE.format(
+ req = requests.get(
+ PUBKEY_ALLOWLIST_URL_TEMPLATE.format(
model=session.model.internal_name.lower()
)
- ).json()
- whitelist = [bytes.fromhex(pk) for pk in whitelist_json["ca_pubkeys"]]
+ )
+ try:
+ req.raise_for_status()
+ allowlist = authentication.AllowList(req.json())
+ except Exception as e:
+ raise click.ClickException(
+ f"Failed to download allow list: {e}"
+ "\nUse --offline to skip the check."
+ ) from e
try:
authentication.authenticate_device(
@@ -468,7 +475,7 @@ def authenticate(
challenge,
p256_root_pubkey=p256_root_bytes,
ed25519_root_pubkey=ed25519_root_bytes,
- whitelist=whitelist,
+ allowlist=allowlist,
)
except authentication.DeviceNotAuthentic:
click.echo("Device is not authentic.")
Why this scored 36/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.