mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-20 05:18:08 +00:00
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
This commit is contained in:
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,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(
|
|
||||||
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:
|
def all_languages(self) -> t.Iterable[str]:
|
||||||
version = blob_version
|
return (lang_file.stem for lang_file in self.path.glob("??.json"))
|
||||||
if common_version is None:
|
|
||||||
common_version = version
|
|
||||||
elif blob_version != common_version:
|
|
||||||
raise ValueError(
|
|
||||||
f"Language {lang} has version {version} but expected {common_version}"
|
|
||||||
)
|
|
||||||
|
|
||||||
blob = translations.blob_from_defs(
|
def generate_single_blob(
|
||||||
blob_json, order, model, version, fonts_dir
|
self,
|
||||||
)
|
lang: str,
|
||||||
all_blobs.append(blob)
|
model: models.TrezorModel,
|
||||||
except Exception as e:
|
version: VersionTuple | None,
|
||||||
import traceback
|
write_version: bool = False,
|
||||||
|
) -> translations.TranslationsBlob:
|
||||||
|
blob_json = self.load_lang(lang)
|
||||||
|
blob_version = translations.version_from_json(blob_json["header"]["version"])
|
||||||
|
|
||||||
traceback.print_exc()
|
if version is None:
|
||||||
LOG.warning(f"Failed to build {lang} for {model.internal_name}: {e}")
|
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
|
||||||
|
|
||||||
|
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
|
continue
|
||||||
|
|
||||||
LOG.info(f"Built {lang} for {model.internal_name}")
|
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:
|
||||||
|
common_version = blob_version
|
||||||
|
elif blob_version != common_version:
|
||||||
|
raise ValueError(
|
||||||
|
f"Language {lang} has version {blob_version} but expected {common_version}"
|
||||||
|
)
|
||||||
|
all_blobs.append(blob)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
|
||||||
return all_blobs
|
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}")
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
@ -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 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…
Reference in New Issue
Block a user