feat(python): implement full certificate verification in trezorctl (fixes #3364)

matejcik/prodtest3
matejcik 5 months ago
parent 5aba19ff06
commit 77848ddf21

@ -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!")

Loading…
Cancel
Save