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/3646/head
matejcik 2 months ago
parent 4bcd6d4f12
commit 9c04ae5c46

@ -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,58 +110,92 @@ 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"
common_version = None def _lang_path(self, lang: str) -> Path:
return self.path / f"{lang}.json"
all_languages = [lang_file.stem for lang_file in HERE.glob("??.json")] def load_lang(self, lang: str) -> translations.JsonDef:
all_blobs: list[translations.TranslationsBlob] = [] return json.loads(self._lang_path(lang).read_text())
for lang in all_languages:
if lang == "en":
continue
for model in ALL_MODELS: def save_lang(self, lang: str, data: translations.JsonDef) -> None:
try: self._lang_path(lang).write_text(json.dumps(data, indent=2) + "\n")
blob_json = json.loads((HERE / f"{lang}.json").read_text())
blob_version = translations.version_from_json( def all_languages(self) -> t.Iterable[str]:
blob_json["header"]["version"] return (lang_file.stem for lang_file in self.path.glob("??.json"))
)
if rewrite_version: def generate_single_blob(
version = current_version self,
if blob_version != current_version: lang: str,
blob_json["header"]["version"] = current_version_str model: models.TrezorModel,
(HERE / f"{lang}.json").write_text( version: VersionTuple | None,
json.dumps(blob_json, indent=2) + "\n" 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)
else: return translations.blob_from_defs(
version = blob_version 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
if version == "auto":
used_version = current_version
elif version == "json":
used_version = None
else:
used_version = version
all_blobs: list[translations.TranslationsBlob] = []
for lang in self.all_languages():
if lang == "en":
continue
for model in ALL_MODELS:
try:
blob = self.generate_single_blob(lang, model, used_version)
blob_version = blob.header.firmware_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}"
) )
all_blobs.append(blob)
except Exception as e:
import traceback
blob = translations.blob_from_defs( traceback.print_exc()
blob_json, order, model, version, fonts_dir LOG.warning(
) f"Failed to build {lang} for {model.internal_name}: {e}"
all_blobs.append(blob) )
except Exception as e: continue
import traceback
traceback.print_exc()
LOG.warning(f"Failed to build {lang} for {model.internal_name}: {e}")
continue
LOG.info(f"Built {lang} for {model.internal_name}") LOG.info(f"Built {lang} for {model.internal_name}")
return all_blobs return all_blobs
def build_all_blobs( def build_all_blobs(
@ -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()
@ -212,11 +263,14 @@ def gen(signed: bool | None) -> None:
raise click.ClickException( raise click.ClickException(
"No matching signature found in signatures.json. Run `cli.py sign` first." "No matching signature found in signatures.json. Run `cli.py sign` first."
) )
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 signature_file["current"]["merkle_root"] != root.hex():
raise click.ClickException( if version_str is None:
f"Merkle root mismatch!\n" # we are using in-tree version. check in-tree merkle root
f"Expected: {root.hex()}\n" if signature_file["current"]["merkle_root"] != root.hex():
f"Stored in signatures.json: {signature_file['current']['merkle_root']}" raise click.ClickException(
) f"Merkle root mismatch!\n"
f"Expected: {root.hex()}\n"
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