Files
libultraship/tools/sign.py
LywxandKenix Whisperwind 787ec0bde4 Implemented dynamic loading (#1051)
* Fully migrated event system into lus

* Fixed tidy

* Last tidy error

* Added some more tidy stuff

* Last pass of tidy stuff

* Implemented dynamic module loading among o2r metadata

* Cleanup some things and moved to built-in libtcc

* Reverted CI dependencies

* Trying to fix libtcc compilation

* Another attempt at trying to fix this

* Fixed GetPlatform error

* Attempt at fixing cross-compilation for libtcc

* Another attempt for ios this time

* Disabled codesign on ios

* Addressed tidy comments

* Implemented keystore system for signature validation

* Renamed some things to manifest

* Fixed small compilation issue

* More tidy fixes

* Fully tested checksum and signature validations

* Added a sign tool

* Renamed missing function

* Removed leftover

* Fixed scripts not loading

* Implemented LoadAll based on dependencies

* Small fix so mods can load high resolution textures

* Added line fixer so it says the real path

* Fixed an issue related to f3d modding

* Fixed tidy

* Shifted some opcodes by one

* Fixed weird rebase shenanigans

* Moved to throw when loading untrusted scripts

* Fixed tidy

* Splitted compile and load for better info on ports

* Fixed tidy again

* Fixed wrong types on event system

* Fixed returning an invalid id when sorting, implemented an internal id tracker among optimizing the sort when registering a new listener

* Removed an unused queue

* Moved HexToBytes into StringHelper and renamed KeystoreSystem to Keystore

* Moved ScriptLoader data to be private

* Replaced default license

* Exported symbols on windows, added variables to set more things on libtcc and changed cmake to tell cmake to export a .lib

* Fixed wrong macro

* Fixed an error when generating .a files

* Fixed tidy

* Fixed more tidy

* I hate tidy

* Disabled readability-identifier-naming for original c extern

* Fixed wrong define

* Removed segmented guard because of false positives

* Fixed validation to verify if address comes from a DLL

* Fixed comment

* Fixed bad cast

* Fixed wrong cast again

* Implemented script loading test

* Fixed missing virtual function

* Added prints to tests

* Fix tests

* Fixed Script System tests

* Fixed last error and a better way to test ModInit and ModExit

* Updated dependencies among adding a tool to generate a keypair

* Added a key load based on user-prompt, trying to fix linux test and updated sign tool to add public_key

* Applied formatting on script_test

* Fixed StringHelper format

* Added missing header

* Updated test to be a ptr on status

* Removed unnecessary stuff

* Fully validated and fixed keystore saving and loading

* Updated geenerate keys script

* Fixed CompileAll

* Addressed latest comments

* Updated autogenerated keys script

* Updated untrusted handler to expose key too

* Added an small warning when not using ed25519 keys

* Added missing python dependencies

* Fixed tidy and updated actions

* Another attempt at fixing windows builds

* Removed search by listener validation because sometimes the same callback is used

* Added an error handler just to avoid crashing the program

* Added better errors for compiler

* Splitted logs into better fitted spdlog calls

* Kinda non-scope but i fixed a big performance issue on shaders

* Added KeyOrigin to know if it comes from the game, from lus or from the user

* Renamed from ScriptSystem to ScriptLoader

* Implemented cleanup on context destroy

* Finish some cleaning

* Wrapped dll load code into an optional ifder

* Fixed linux tests

* Enabled dll loading in test

* Inverted define

* Populates initial key list

* Moves keys to root of LUS

---------

Co-authored-by: Kenix Whisperwind <kenixwhisperwind@gmail.com>
2026-04-13 22:40:35 -04:00

98 lines
3.8 KiB
Python

import argparse
import hashlib
import json
import os
import shutil
import sys
import tempfile
import zipfile
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
def update_zip_manifest(zip_path: str, public_key_hex: str, checksum_hex: str, signature_hex: str):
manifest_data = {}
with zipfile.ZipFile(zip_path, 'r') as zin:
if "manifest.json" in zin.namelist():
try:
existing_manifest = zin.read("manifest.json").decode('utf-8')
manifest_data = json.loads(existing_manifest)
except json.JSONDecodeError:
print("Warning: Existing manifest.json is invalid JSON. Overwriting.")
manifest_data["checksum"] = checksum_hex
manifest_data["signature"] = signature_hex
manifest_data["public_key"] = public_key_hex
fd, temp_path = tempfile.mkstemp(suffix=".zip")
os.close(fd)
try:
with zipfile.ZipFile(zip_path, 'r') as zin:
with zipfile.ZipFile(temp_path, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
if item.filename != "manifest.json":
zout.writestr(item, zin.read(item.filename))
new_manifest_bytes = json.dumps(manifest_data, indent=4).encode('utf-8')
zout.writestr("manifest.json", new_manifest_bytes)
shutil.move(temp_path, zip_path)
except Exception as e:
if os.path.exists(temp_path):
os.remove(temp_path)
raise e
def load_private_key(key_path: str, password: bytes = None):
try:
with open(key_path, "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=password
)
if not isinstance(private_key, ed25519.Ed25519PrivateKey):
print(f"Error: The key at '{key_path}' is not an Ed25519 private key.")
sys.exit(1)
return private_key
except FileNotFoundError:
print(f"Error: Private key file not found at '{key_path}'")
sys.exit(1)
def calculate_o2r_checksum(zip_path) -> bytes:
hasher = hashlib.blake2b()
with zipfile.ZipFile(zip_path, 'r') as zf:
all_entries = zf.namelist()
filtered_files = [f for f in all_entries if not f.endswith('/') and f != 'manifest.json']
filtered_files.sort()
for file_name in filtered_files:
hasher.update(file_name.encode('utf-8'))
with zf.open(file_name) as f:
for chunk in iter(lambda: f.read(8192), b""):
hasher.update(chunk)
return hasher.digest()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Sign a ZIP and inject into manifest.json.")
parser.add_argument("zip_file", help="Path to the target .zip file")
parser.add_argument("private_key", help="Path to the PEM private key")
parser.add_argument("-p", "--passphrase", help="Passphrase for the PEM file", default=None)
args = parser.parse_args()
print(f"Processing {args.zip_file}...")
raw_checksum = calculate_o2r_checksum(args.zip_file)
hex_checksum = raw_checksum.hex()
passphrase_bytes = args.passphrase.encode('utf-8') if args.passphrase else None
private_key = load_private_key(args.private_key, password=passphrase_bytes)
public_key_obj = private_key.public_key()
raw_public_key = public_key_obj.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
public_key_hex = raw_public_key.hex()
signature_str = private_key.sign(raw_checksum).hex()
update_zip_manifest(args.zip_file, public_key_hex, hex_checksum, signature_str)
print("[SUCCESS] Zip signed successfully.")