feat(core/translations): add support for explicitly specifying blob version

so that it's possible to re-sign translation blobs on a specific commit
for older firmware version
pull/3653/head
matejcik 2 months ago committed by matejcik
parent 229a06d3a2
commit 6918b16313

@ -11,6 +11,7 @@ import click
from trezorlib import cosi, models, merkle_tree from trezorlib import cosi, models, merkle_tree
from trezorlib._internal import translations from trezorlib._internal import translations
from trezorlib._internal.translations import VersionTuple
HERE = Path(__file__).parent.resolve() HERE = Path(__file__).parent.resolve()
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -36,6 +37,7 @@ class SignedInfo(t.TypedDict):
signature: str signature: str
datetime: str datetime: str
commit: str commit: str
version: str
class UnsignedInfo(t.TypedDict): class UnsignedInfo(t.TypedDict):
@ -49,7 +51,7 @@ class SignatureFile(t.TypedDict):
history: list[SignedInfo] history: list[SignedInfo]
def _version_from_version_h() -> translations.VersionTuple: def _version_from_version_h() -> VersionTuple:
defines: t.Dict[str, int] = {} defines: t.Dict[str, int] = {}
with open(VERSION_H) as f: with open(VERSION_H) as f:
for line in f: for line in f:
@ -69,6 +71,10 @@ def _version_from_version_h() -> translations.VersionTuple:
) )
def _version_str(version: VersionTuple) -> str:
return ".".join(str(v) for v in version)
def make_tree_info(merkle_root: bytes) -> UnsignedInfo: def make_tree_info(merkle_root: bytes) -> UnsignedInfo:
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
commit = ( commit = (
@ -81,8 +87,10 @@ def make_tree_info(merkle_root: bytes) -> UnsignedInfo:
) )
def sign_info(info: UnsignedInfo, signature: bytes) -> SignedInfo: def sign_info(
return SignedInfo(signature=signature.hex(), **info) info: UnsignedInfo, signature: bytes, version: VersionTuple
) -> SignedInfo:
return SignedInfo(signature=signature.hex(), version=_version_str(version), **info)
def update_merkle_root(signature_file: SignatureFile, merkle_root: bytes) -> bool: def update_merkle_root(signature_file: SignatureFile, merkle_root: bytes) -> bool:
@ -102,53 +110,87 @@ def update_merkle_root(signature_file: SignatureFile, merkle_root: bytes) -> boo
return True return True
def generate_all_blobs(rewrite_version: bool) -> list[translations.TranslationsBlob]: class TranslationsDir:
order = translations.order_from_json(json.loads((HERE / "order.json").read_text())) def __init__(self, path: Path = HERE):
fonts_dir = HERE / "fonts" self.path = path
self.order = translations.order_from_json(
json.loads((self.path / "order.json").read_text())
)
current_version = _version_from_version_h() @property
current_version_str = ".".join(str(v) for v in current_version) def fonts_dir(self) -> Path:
return self.path / "fonts"
def _lang_path(self, lang: str) -> Path:
return self.path / f"{lang}.json"
def load_lang(self, lang: str) -> translations.JsonDef:
return json.loads(self._lang_path(lang).read_text())
def save_lang(self, lang: str, data: translations.JsonDef) -> None:
self._lang_path(lang).write_text(json.dumps(data, indent=2) + "\n")
def all_languages(self) -> t.Iterable[str]:
return (lang_file.stem for lang_file in self.path.glob("??.json"))
def generate_single_blob(
self,
lang: str,
model: models.TrezorModel,
version: VersionTuple | None,
write_version: bool = False,
) -> translations.TranslationsBlob:
blob_json = self.load_lang(lang)
blob_version = translations.version_from_json(blob_json["header"]["version"])
if version is None:
version = blob_version
if write_version and blob_version != version:
blob_json["header"]["version"] = _version_str(version)
self.save_lang(lang, blob_json)
return translations.blob_from_defs(
blob_json, self.order, model, version, self.fonts_dir
)
def generate_all_blobs(
self,
version: VersionTuple | t.Literal["auto"] | t.Literal["json"],
) -> list[translations.TranslationsBlob]:
current_version = _version_from_version_h()
common_version = None common_version = None
all_languages = [lang_file.stem for lang_file in HERE.glob("??.json")] if version == "auto":
used_version = current_version
elif version == "json":
used_version = None
else:
used_version = version
all_blobs: list[translations.TranslationsBlob] = [] all_blobs: list[translations.TranslationsBlob] = []
for lang in all_languages: for lang in self.all_languages():
if lang == "en": if lang == "en":
continue continue
for model in ALL_MODELS: for model in ALL_MODELS:
try: try:
blob_json = json.loads((HERE / f"{lang}.json").read_text()) blob = self.generate_single_blob(lang, model, used_version)
blob_version = translations.version_from_json( blob_version = blob.header.firmware_version
blob_json["header"]["version"]
)
if rewrite_version:
version = current_version
if blob_version != current_version:
blob_json["header"]["version"] = current_version_str
(HERE / f"{lang}.json").write_text(
json.dumps(blob_json, indent=2) + "\n"
)
else:
version = blob_version
if common_version is None: if common_version is None:
common_version = version common_version = blob_version
elif blob_version != common_version: elif blob_version != common_version:
raise ValueError( raise ValueError(
f"Language {lang} has version {version} but expected {common_version}" f"Language {lang} has version {blob_version} but expected {common_version}"
)
blob = translations.blob_from_defs(
blob_json, order, model, version, fonts_dir
) )
all_blobs.append(blob) all_blobs.append(blob)
except Exception as e: except Exception as e:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
LOG.warning(f"Failed to build {lang} for {model.internal_name}: {e}") LOG.warning(
f"Failed to build {lang} for {model.internal_name}: {e}"
)
continue continue
LOG.info(f"Built {lang} for {model.internal_name}") LOG.info(f"Built {lang} for {model.internal_name}")
@ -189,12 +231,21 @@ def cli() -> None:
@cli.command() @cli.command()
@click.option("--signed", is_flag=True, help="Generate signed blobs.") @click.option("--signed", is_flag=True, help="Generate signed blobs.")
def gen(signed: bool | None) -> None: @click.option(
"--version", "version_str", help="Set the blob version independent of JSON data."
)
def gen(signed: bool | None, version_str: str | None) -> None:
"""Generate all language blobs for all models. """Generate all language blobs for all models.
The generated blobs will be signed with the development keys. The generated blobs will be signed with the development keys.
""" """
all_blobs = generate_all_blobs(rewrite_version=True) if version_str is not None:
version = translations.version_from_json(version_str)
else:
version = "auto"
tdir = TranslationsDir()
all_blobs = tdir.generate_all_blobs(version)
tree = merkle_tree.MerkleTree(b.header_bytes for b in all_blobs) tree = merkle_tree.MerkleTree(b.header_bytes for b in all_blobs)
root = tree.get_root_hash() root = tree.get_root_hash()
@ -216,7 +267,10 @@ def gen(signed: bool | None) -> None:
signature = cosi.sign_with_privkeys(root, PRIVATE_KEYS_DEV) signature = cosi.sign_with_privkeys(root, PRIVATE_KEYS_DEV)
sigmask = 0b111 sigmask = 0b111
build_all_blobs(all_blobs, tree, sigmask, signature) build_all_blobs(all_blobs, tree, sigmask, signature)
if update_merkle_root(signature_file, root):
if version_str is not None:
click.echo("Skipping Merkle root update because of explicit version.")
elif update_merkle_root(signature_file, root):
SIGNATURES_JSON.write_text(json.dumps(signature_file, indent=2) + "\n") SIGNATURES_JSON.write_text(json.dumps(signature_file, indent=2) + "\n")
click.echo("Updated signatures.json") click.echo("Updated signatures.json")
else: else:
@ -224,12 +278,27 @@ def gen(signed: bool | None) -> None:
@cli.command() @cli.command()
def merkle_root() -> None: @click.option(
"--version", "version_str", help="Set the blob version independent of JSON data."
)
def merkle_root(version_str: str | None) -> None:
"""Print the Merkle root of all language blobs.""" """Print the Merkle root of all language blobs."""
all_blobs = generate_all_blobs(rewrite_version=False) if version_str is None:
version = "json"
else:
version = translations.version_from_json(version_str)
tdir = TranslationsDir()
all_blobs = tdir.generate_all_blobs(version)
tree = merkle_tree.MerkleTree(b.header_bytes for b in all_blobs) tree = merkle_tree.MerkleTree(b.header_bytes for b in all_blobs)
root = tree.get_root_hash() root = tree.get_root_hash()
if version_str is not None:
# short-circuit: just print the Merkle root
click.echo(root.hex())
return
# we are using in-tree version. check in-tree merkle root
signature_file: SignatureFile = json.loads(SIGNATURES_JSON.read_text()) signature_file: SignatureFile = json.loads(SIGNATURES_JSON.read_text())
if signature_file["current"]["merkle_root"] != root.hex(): if signature_file["current"]["merkle_root"] != root.hex():
raise click.ClickException( raise click.ClickException(
@ -245,23 +314,37 @@ def merkle_root() -> None:
@cli.command() @cli.command()
@click.argument("signature_hex") @click.argument("signature_hex")
@click.option("--force", is_flag=True, help="Write even if the signature is invalid.") @click.option("--force", is_flag=True, help="Write even if the signature is invalid.")
def sign(signature_hex: str, force: bool | None) -> None: @click.option(
"--version", "version_str", help="Set the blob version independent of JSON data."
)
def sign(signature_hex: str, force: bool | None, version_str: str | None) -> None:
"""Insert a signature into language blobs.""" """Insert a signature into language blobs."""
all_blobs = generate_all_blobs(rewrite_version=False) if version_str is None:
version = "json"
else:
version = translations.version_from_json(version_str)
tdir = TranslationsDir()
all_blobs = tdir.generate_all_blobs(version)
tree = merkle_tree.MerkleTree(b.header_bytes for b in all_blobs) tree = merkle_tree.MerkleTree(b.header_bytes for b in all_blobs)
root = tree.get_root_hash() root = tree.get_root_hash()
blob_version = all_blobs[0].header.firmware_version
signature_file: SignatureFile = json.loads(SIGNATURES_JSON.read_text()) signature_file: SignatureFile = json.loads(SIGNATURES_JSON.read_text())
if version_str is None:
# we are using in-tree version. check in-tree merkle root
if signature_file["current"]["merkle_root"] != root.hex(): if signature_file["current"]["merkle_root"] != root.hex():
raise click.ClickException( raise click.ClickException(
f"Merkle root mismatch!\n" f"Merkle root mismatch!\n"
f"Expected: {root.hex()}\n" f"Expected: {root.hex()}\n"
f"Stored in signatures.json: {signature_file['current']['merkle_root']}" f"Stored in signatures.json: {signature_file['current']['merkle_root']}"
) )
# else, proceed with the calculated Merkle root
# Update signature file data. It will be written only if the signature verifies. # Update signature file data. It will be written only if the signature verifies.
tree_info = make_tree_info(root) tree_info = make_tree_info(root)
signed_info = sign_info(tree_info, bytes.fromhex(signature_hex)) signed_info = sign_info(tree_info, bytes.fromhex(signature_hex), blob_version)
signature_file["history"].insert(0, signed_info) signature_file["history"].insert(0, signed_info)
signature_bytes = bytes.fromhex(signature_hex) signature_bytes = bytes.fromhex(signature_hex)

@ -9,7 +9,8 @@
"merkle_root": "7393c46812b1bdf8789adcfde4feea951c7036c7524d9b38f64641620504898e", "merkle_root": "7393c46812b1bdf8789adcfde4feea951c7036c7524d9b38f64641620504898e",
"signature": "03ca77980a972b9ff9825a67d5be437f2588f095104fefc9d0415702f065fc063f9f30ce69768bf1b3c8ae7e93220c61f1e3a6d0d1cc52b8ddae6bd1766bd4f202", "signature": "03ca77980a972b9ff9825a67d5be437f2588f095104fefc9d0415702f065fc063f9f30ce69768bf1b3c8ae7e93220c61f1e3a6d0d1cc52b8ddae6bd1766bd4f202",
"datetime": "2024-03-07T11:26:08.409760", "datetime": "2024-03-07T11:26:08.409760",
"commit": "45e8a842a31e62a6d43d7f6ccac62a45e1198ef0" "commit": "45e8a842a31e62a6d43d7f6ccac62a45e1198ef0",
"version": "2.7.0.0"
} }
] ]
} }

Loading…
Cancel
Save