diff --git a/python/src/trezorlib/cli/device.py b/python/src/trezorlib/cli/device.py index 6ca2e9b0d..226eeb77c 100644 --- a/python/src/trezorlib/cli/device.py +++ b/python/src/trezorlib/cli/device.py @@ -16,7 +16,7 @@ import secrets import sys -from typing import TYPE_CHECKING, Optional, Sequence +from typing import TYPE_CHECKING, BinaryIO, Optional, Sequence import click @@ -334,17 +334,161 @@ def set_busy( return device.set_busy(client, expiry * 1000) +ROOT_PUBKEY_TS3 = bytes.fromhex( + "04ca97480ac0d7b1e6efafe518cd433cec2bf8ab9822d76eafd34363b55d63e60" + "380bff20acc75cde03cffcb50ab6f8ce70c878e37ebc58ff7cca0a83b16b15fa5" +) + +TS3_CA_WHITELIST = [ + bytes.fromhex(x) + for x in ( + "04b12efa295ad825a534b7c0bf276e93ad116434426763fa87bfa8a2f12e726906dcf566813f62eba8f8795f94dba0391c53682809cbbd7e4ba01d960b4f1c68f1", + "04cb87d4c5d0fd5854e829f4c1b666e49a86c25c88a904c0feb66f1338faed0d7760010d7ea1a6474cbcfe1143bd4b5397a4e8b7fe86899113caecf42a984b0c0f", + "0450c45878b2c6403a5a16e97a8957dc3ea36919bce9321b357f6e7ebe6257ee54102a2c2fa5cefed1dabc498fc76dc0bcf3c3a8a415eac7cc32e7c18185f25b0d", + ) +] + +DEV_ROOT_PUBKEY_TS3 = bytes.fromhex( + "047f77368dea2d4d61e989f474a56723c3212dacf8a808d8795595ef38441427c" + "4389bc454f02089d7f08b873005e4c28d432468997871c0bf286fd3861e21e96a" +) + + @cli.command() @click.argument("hex_challenge", required=False) +@click.option("-R", "--root", type=click.File("rb"), help="Custom root certificate.") +@click.option( + "-r", "--raw", is_flag=True, help="Print raw cryptographic data and exit." +) +@click.option( + "-n", + "--offline", + is_flag=True, + help="Do not download the public key whitelist from Trezor servers.", +) @with_client -def authenticate(client: "TrezorClient", hex_challenge: Optional[str]) -> None: - """Get information to verify the authenticity of the device.""" +def authenticate( + client: "TrezorClient", + hex_challenge: Optional[str], + root: Optional[BinaryIO], + raw: Optional[bool], + offline: Optional[bool], +) -> 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 import the 'cryptography' library, decode + the signatures and check their authenticity. By default, it will try to download + the public key whitelist from Trezor servers. It is strongly recommended to do this + step, however, you can skip it by passing the --offline option. + + When not using --raw, 'cryptography' library is required. You can install it via: + + pip3 install cryptography + """ if hex_challenge is None: hex_challenge = secrets.token_hex(32) - click.echo(f"Challenge: {hex_challenge}") + challenge = bytes.fromhex(hex_challenge) msg = device.authenticate(client, challenge) - click.echo(f"Signature of challenge: {msg.signature.hex()}") - click.echo(f"Device certificate: {msg.certificates[0].hex()}") - for cert in msg.certificates[1:]: - click.echo(f"CA certificate: {cert.hex()}") + + if raw: + click.echo(f"Challenge: {hex_challenge}") + click.echo(f"Signature of challenge: {msg.signature.hex()}") + click.echo(f"Device certificate: {msg.certificates[0].hex()}") + for cert in msg.certificates[1:]: + click.echo(f"CA certificate: {cert.hex()}") + return + + try: + import cryptography + + version = [int(x) for x in cryptography.__version__.split(".")] + if version[0] < 40: + click.echo( + "You need to upgrade the 'cryptography' library to verify the signature." + ) + click.echo("You can do so by running:") + click.echo(" pip3 install --upgrade cryptography") + sys.exit(1) + + except ImportError: + click.echo( + "You need to install the 'cryptography' library to verify the signature." + ) + click.echo("You can do so by running:") + click.echo(" pip3 install cryptography") + sys.exit(1) + + from cryptography import x509 + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import ec + + CHALLENGE_HEADER = b"AuthenticateDevice:" + challenge_bytes = ( + len(CHALLENGE_HEADER).to_bytes(1, "big") + + CHALLENGE_HEADER + + len(challenge).to_bytes(1, "big") + + challenge + ) + + first_cert = x509.load_der_x509_certificate(msg.certificates[0]) + first_cert.public_key().verify( + msg.signature, challenge_bytes, ec.ECDSA(hashes.SHA256()) + ) + click.echo("Challenge verified successfully.") + + for issuer in msg.certificates[1:]: + cert = x509.load_der_x509_certificate(issuer) + if not offline: + issuer_pk = cert.public_key().public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + if issuer_pk not in TS3_CA_WHITELIST: + click.echo( + f"Certificate {cert.subject.rfc4514_string()} was issued by an unknown CA." + ) + raise click.ClickException("Failed to verify certificate chain!") + + cert.public_key().verify( + first_cert.signature, + first_cert.tbs_certificate_bytes, + first_cert.signature_algorithm_parameters, + ) + click.echo( + f"Certificate {first_cert.subject.rfc4514_string()} verified successfully." + ) + first_cert = cert + + if root is not None: + root_cert = x509.load_der_x509_certificate(root.read()) + roots = [("the specified root certificate", root_cert.public_key())] + else: + prod_root = ec.EllipticCurvePublicKey.from_encoded_point( + ec.SECP256R1(), ROOT_PUBKEY_TS3 + ) + dev_root = ec.EllipticCurvePublicKey.from_encoded_point( + ec.SECP256R1(), DEV_ROOT_PUBKEY_TS3 + ) + roots = [ + ("Trezor Company", prod_root), + ("TESTING ENVIRONMENT. DO NOT USE THIS DEVICE", dev_root), + ] + + for issuer, pubkey in roots: + try: + pubkey.verify( + first_cert.signature, + first_cert.tbs_certificate_bytes, + first_cert.signature_algorithm_parameters, + ) + click.echo( + f"Certificate {first_cert.subject.rfc4514_string()} was issued by {issuer}." + ) + return + except Exception: + continue + + raise click.ClickException("Failed to verify certificate chain!")