Create cert.sys during EmuNAND title installation if not found

This commit is contained in:
Campbell 2025-07-12 13:03:42 -04:00
parent 6d38df9133
commit ce5d118de1
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
2 changed files with 39 additions and 4 deletions

View File

@ -34,7 +34,7 @@ class EmuNAND:
""" """
def __init__(self, emunand_root: str | pathlib.Path, callback: Callable | None = None): def __init__(self, emunand_root: str | pathlib.Path, callback: Callable | None = None):
self.emunand_root = pathlib.Path(emunand_root) self.emunand_root = pathlib.Path(emunand_root)
self.log = callback if callback is not None else None self.log = callback if callback is not None else lambda x: None
self.import_dir = self.emunand_root.joinpath("import") self.import_dir = self.emunand_root.joinpath("import")
self.meta_dir = self.emunand_root.joinpath("meta") self.meta_dir = self.emunand_root.joinpath("meta")
@ -70,12 +70,14 @@ class EmuNAND:
skip_hash : bool, optional skip_hash : bool, optional
Skip the hash check and install the title regardless of its hashes. Defaults to false. Skip the hash check and install the title regardless of its hashes. Defaults to false.
""" """
self.log(f"[PROGRESS] Starting install of title with Title ID {title.tmd.title_id}...")
# Save the upper and lower portions of the Title ID, because these are used as target install directories. # Save the upper and lower portions of the Title ID, because these are used as target install directories.
tid_upper = title.tmd.title_id[:8] tid_upper = title.tmd.title_id[:8]
tid_lower = title.tmd.title_id[8:] tid_lower = title.tmd.title_id[8:]
# Tickets are installed as <tid_lower>.tik in /ticket/<tid_upper>/ # Tickets are installed as <tid_lower>.tik in /ticket/<tid_upper>/
ticket_dir = self.ticket_dir.joinpath(tid_upper) ticket_dir = self.ticket_dir.joinpath(tid_upper)
self.log(f"[PROGRESS] Installing ticket to \"{ticket_dir}\"...")
ticket_dir.mkdir(exist_ok=True) ticket_dir.mkdir(exist_ok=True)
ticket_dir.joinpath(f"{tid_lower}.tik").write_bytes(title.ticket.dump()) ticket_dir.joinpath(f"{tid_lower}.tik").write_bytes(title.ticket.dump())
@ -86,19 +88,25 @@ class EmuNAND:
title_dir = title_dir.joinpath(tid_lower) title_dir = title_dir.joinpath(tid_lower)
title_dir.mkdir(exist_ok=True) title_dir.mkdir(exist_ok=True)
content_dir = title_dir.joinpath("content") content_dir = title_dir.joinpath("content")
self.log(f"[PROGRESS] Installing TMD to \"{content_dir}\"...")
if content_dir.exists(): if content_dir.exists():
shutil.rmtree(content_dir) # Clear the content directory so old contents aren't left behind. shutil.rmtree(content_dir) # Clear the content directory so old contents aren't left behind.
content_dir.mkdir(exist_ok=True) content_dir.mkdir(exist_ok=True)
content_dir.joinpath("title.tmd").write_bytes(title.tmd.dump()) content_dir.joinpath("title.tmd").write_bytes(title.tmd.dump())
self.log(f"[PROGRESS] Installing content to \"{content_dir}\"...")
if skip_hash:
self.log("[WARN] Not checking content hashes! Content validity will not be verified.")
for content_file in range(0, title.tmd.num_contents): for content_file in range(0, title.tmd.num_contents):
if title.tmd.content_records[content_file].content_type == 1: if title.tmd.content_records[content_file].content_type == 1:
content_file_name = f"{title.tmd.content_records[content_file].content_id:08X}".lower() content_file_name = f"{title.tmd.content_records[content_file].content_id:08X}".lower()
self.log(f"[PROGRESS] Installing content \"{content_file_name}.app\" to \"{content_dir}\"... ")
content_dir.joinpath(f"{content_file_name}.app").write_bytes( content_dir.joinpath(f"{content_file_name}.app").write_bytes(
title.get_content_by_index(content_file, skip_hash=skip_hash)) title.get_content_by_index(content_file, skip_hash=skip_hash))
title_dir.joinpath("data").mkdir(exist_ok=True) # Empty directory used for save data for the title. title_dir.joinpath("data").mkdir(exist_ok=True) # Empty directory used for save data for the title.
# Shared contents need to be installed to /shared1/, with incremental names determined by /shared1/content.map. # Shared contents need to be installed to /shared1/, with incremental names determined by /shared1/content.map.
content_map_path = self.shared1_dir.joinpath("content.map") content_map_path = self.shared1_dir.joinpath("content.map")
self.log(f"[PROGRESS] Installing shared content to \"{self.shared1_dir}\"...")
content_map = _SharedContentMap() content_map = _SharedContentMap()
existing_hashes = [] existing_hashes = []
if content_map_path.exists(): if content_map_path.exists():
@ -108,7 +116,10 @@ class EmuNAND:
for content_file in range(0, title.tmd.num_contents): for content_file in range(0, title.tmd.num_contents):
if title.tmd.content_records[content_file].content_type == 32769: if title.tmd.content_records[content_file].content_type == 32769:
if title.tmd.content_records[content_file].content_hash not in existing_hashes: if title.tmd.content_records[content_file].content_hash not in existing_hashes:
self.log(f"[PROGRESS] Adding shared content hash to content.map...")
content_file_name = content_map.add_content(title.tmd.content_records[content_file].content_hash) content_file_name = content_map.add_content(title.tmd.content_records[content_file].content_hash)
self.log(f"[PROGRESS] Installing shared content \"{content_file_name}.app\" to "
f"\"{self.shared1_dir}\"...")
self.shared1_dir.joinpath(f"{content_file_name}.app").write_bytes( self.shared1_dir.joinpath(f"{content_file_name}.app").write_bytes(
title.get_content_by_index(content_file, skip_hash=skip_hash)) title.get_content_by_index(content_file, skip_hash=skip_hash))
self.shared1_dir.joinpath("content.map").write_bytes(content_map.dump()) self.shared1_dir.joinpath("content.map").write_bytes(content_map.dump())
@ -120,6 +131,7 @@ class EmuNAND:
meta_dir = self.meta_dir.joinpath(tid_upper) meta_dir = self.meta_dir.joinpath(tid_upper)
meta_dir.mkdir(exist_ok=True) meta_dir.mkdir(exist_ok=True)
meta_dir = meta_dir.joinpath(tid_lower) meta_dir = meta_dir.joinpath(tid_lower)
self.log(f"[PROGRESS] Installing meta data to \"{meta_dir}\"...")
meta_dir.mkdir(exist_ok=True) meta_dir.mkdir(exist_ok=True)
meta_dir.joinpath("title.met").write_bytes(title.wad.get_meta_data()) meta_dir.joinpath("title.met").write_bytes(title.wad.get_meta_data())
@ -127,12 +139,26 @@ class EmuNAND:
uid_sys_path = self.sys_dir.joinpath("uid.sys") uid_sys_path = self.sys_dir.joinpath("uid.sys")
uid_sys = _UidSys() uid_sys = _UidSys()
if not uid_sys_path.exists(): if not uid_sys_path.exists():
self.log("[WARN] uid.sys does not exist! Creating it with the default entry.")
uid_sys.create() uid_sys.create()
else: else:
uid_sys.load(uid_sys_path.read_bytes()) uid_sys.load(uid_sys_path.read_bytes())
self.log("[PROGRESS] Adding title to uid.sys and assigning a new UID...")
uid_sys.add(title.tmd.title_id) uid_sys.add(title.tmd.title_id)
uid_sys_path.write_bytes(uid_sys.dump()) uid_sys_path.write_bytes(uid_sys.dump())
# Check for a cert.sys and initialize it using the certs in the WAD if it doesn't exist.
cert_sys_path = self.sys_dir.joinpath("cert.sys")
if not cert_sys_path.exists():
self.log("[WARN] cert.sys does not exist! Creating it using certs from the installed title...")
cert_sys_data = b''
cert_sys_data += title.cert_chain.ticket_cert.dump()
cert_sys_data += title.cert_chain.ca_cert.dump()
cert_sys_data += title.cert_chain.tmd_cert.dump()
cert_sys_path.write_bytes(cert_sys_data)
self.log("[PROGRESS] Completed title installation.")
def uninstall_title(self, tid: str) -> None: def uninstall_title(self, tid: str) -> None:
""" """
Uninstall the Title with the specified Title ID from the EmuNAND. This will leave shared contents unmodified. Uninstall the Title with the specified Title ID from the EmuNAND. This will leave shared contents unmodified.

View File

@ -134,9 +134,12 @@ def download_tmd(title_id: str, title_version: int | None = None, wiiu_endpoint:
else: else:
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.") raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
# Handle a 404 if the TID/version doesn't exist. # Handle a 404 if the TID/version doesn't exist.
if response.status_code != 200: if response.status_code == 404:
raise ValueError("The requested Title ID or TMD version does not exist. Please check the Title ID and Title" raise ValueError("The requested Title ID or TMD version does not exist. Please check the Title ID and Title"
" version and then try again.") " version and then try again.")
elif response.status_code != 200:
raise Exception(f"An unknown error occurred while downloading the TMD. "
f"Got HTTP status code: {response.status_code}")
total_size = int(response.headers["Content-Length"]) total_size = int(response.headers["Content-Length"])
progress(0, total_size) progress(0, total_size)
# Stream the TMD's data in chunks so that we can post updates to the callback function (assuming one was supplied). # Stream the TMD's data in chunks so that we can post updates to the callback function (assuming one was supplied).
@ -198,9 +201,12 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_overrid
"override is valid.") "override is valid.")
else: else:
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.") raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
if response.status_code != 200: if response.status_code == 404:
raise ValueError("The requested Title ID does not exist, or refers to a non-free title. Tickets can only" raise ValueError("The requested Title ID does not exist, or refers to a non-free title. Tickets can only"
" be downloaded for titles that are free on the NUS.") " be downloaded for titles that are free on the NUS.")
elif response.status_code != 200:
raise Exception(f"An unknown error occurred while downloading the Ticket. "
f"Got HTTP status code: {response.status_code}")
total_size = int(response.headers["Content-Length"]) total_size = int(response.headers["Content-Length"])
progress(0, total_size) progress(0, total_size)
# Stream the Ticket's data just like with the TMD. # Stream the Ticket's data just like with the TMD.
@ -316,10 +322,13 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
"override is valid.") "override is valid.")
else: else:
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.") raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
if response.status_code != 200: if response.status_code == 404:
raise ValueError("The requested Title ID does not exist, or an invalid Content ID is present in the" raise ValueError("The requested Title ID does not exist, or an invalid Content ID is present in the"
" content records provided.\n Failed while downloading Content ID: 000000" + " content records provided.\n Failed while downloading Content ID: 000000" +
content_id_hex) content_id_hex)
elif response.status_code != 200:
raise Exception(f"An unknown error occurred while downloading the content. "
f"Got HTTP status code: {response.status_code}")
total_size = int(response.headers["Content-Length"]) total_size = int(response.headers["Content-Length"])
progress(0, total_size) progress(0, total_size)
# Stream the content just like the TMD/Ticket. # Stream the content just like the TMD/Ticket.