21 Commits

Author SHA1 Message Date
c4cb028385 Re-add wiiload module post-merge 2025-01-23 22:27:05 -05:00
7ee5c1d728 Added compress_lz77 docstrings, temporarily removed unfinished wiiload module 2025-01-23 22:25:04 -05:00
e9a110bb1e LZ77 compression is now fully functional! (But still very slow) 2025-01-23 21:27:15 -05:00
d6d0af0623 Updated WIP LZ77 compressor, still not working yet (again) 2025-01-20 22:22:09 -05:00
6916324479 Updated WIP LZ77 compressor, still not working yet 2025-01-19 21:50:42 -05:00
89b0dca624 Merge branch 'main' into unfinished 2025-01-16 22:47:20 -05:00
e7070b6758 Unfinished wiiload module and LZ77 compression code 2025-01-16 22:44:28 -05:00
93790d6f58 Added docs for LZ77 module 2025-01-16 22:43:51 -05:00
f0b79e1f39 Match Root-CA00000002-XS00000004 as a dev Ticket 2025-01-12 22:23:32 -05:00
06b36290ed Add error handling for custom NUS endpoints 2025-01-12 21:54:16 -05:00
47472e7b94 Added LZ77 decompression module 2025-01-08 18:43:48 -05:00
7c5af6ebe0 Always user all uppercase when getting installed titles on EmuNAND 2025-01-02 17:20:09 -05:00
046645eb56 Added method to title module to get if a title is signed legitimately 2024-12-23 23:50:14 -05:00
e45c7a3076 (docs) Began writing module descriptions 2024-12-21 18:09:37 -05:00
c2f6225500 Entirely restructured API documentation, now much easier to navigate 2024-12-20 19:21:53 -05:00
04d17a58d2 Added missing note about retail vs development root keys 2024-12-16 22:24:27 -05:00
aa9e8fb4ea Updated placeholder docstrings in cert module 2024-12-16 22:22:11 -05:00
8a15b1e82e Handle cert chain data in a title as a CertificateChain instead of bytes 2024-12-16 22:14:07 -05:00
ece19177c4 Added error handling to cert module, added support for dev CA cert 2024-12-16 21:55:58 -05:00
3a44eaf2cf Added new CertificateChain class to manage the certs in a chain
Some checks failed
Build and publish documentation with Sphinx / build (push) Has been cancelled
Build and publish documentation with Sphinx / deploy (push) Has been cancelled
2024-12-15 23:09:13 -05:00
2fdd808137 Added new cert module to parse certs and functions to use them for verification 2024-12-15 22:08:51 -05:00
44 changed files with 1137 additions and 258 deletions

23
docs/source/api.md Normal file
View File

@@ -0,0 +1,23 @@
# API Documentation
libWiiPy is divided up into a few subpackages to organize related features.
| Package | Description |
|--------------------------------------|-----------------------------------------------------------------|
| [libWiiPy.archive](/archive/archive) | Used to pack and extract archive formats used on the Wii |
| [libWiiPy.media](/media/media) | Used for parsing and manipulating media formats used on the Wii |
| [libWiiPy.nand](/nand/nand) | Used for working with EmuNANDs and core system files on the Wii |
| [libWiiPy.title](/title/title) | Used for parsing and manipulating Wii titles |
When using libWiiPy in your project, you can choose to either only import the package that you need, or you can use `import libWiiPy` to import the entire package, which each module being available at `libWiiPy.<package>.<module>`.
## Full Package Contents
```{toctree}
:maxdepth: 8
/archive/archive
/media/media
/nand/nand
/title/title
```

View File

@@ -0,0 +1,20 @@
# libWiiPy.archive Package
## Modules
The `libWiiPy.archive` package contains modules for packing and extracting archive formats used by the Wii. This currently includes packing and unpacking support for U8 archives and decompression support for ASH archives.
| Module | Description |
|----------------------------------------|---------------------------------------------------------|
| [libWiiPy.archive.ash](/archive/ash) | Provides support for decompressing ASH archives |
| [libWiiPy.archive.lz77](/archive/lz77) | Provides support for the LZ77 compression scheme |
| [libWiiPy.archive.u8](/archive/u8) | Provides support for packing and extracting U8 archives |
### libWiiPy.archive Package Contents
```{toctree}
:maxdepth: 4
/archive/ash
/archive/lz77
/archive/u8
```

View File

@@ -0,0 +1,14 @@
# libWiiPy.archive.ash Module
The `libWiiPy.archive.ash` module provides support for handling ASH files, which are a compressed format primarily used in the Wii Menu, but also in some other titles such as My Pokémon Ranch.
At present, libWiiPy only has support for decompressing ASH files, with compression as a planned feature for the future.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.archive.ash
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -0,0 +1,12 @@
# libWiiPy.archive.lz77 Module
The `libWiiPy.archive.lz77` module provides support for handling LZ77 compression, which is a compression format used across the Wii and other Nintendo consoles.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.archive.lz77
:members:
:undoc-members:
:show-inheritance:
```

14
docs/source/archive/u8.md Normal file
View File

@@ -0,0 +1,14 @@
# libWiiPy.archive.u8 Module
The `libWiiPy.archive.u8` module provides support for handling U8 archives, which are a non-compressed archive format used extensively on the Wii to join multiple files into one.
This module exposes functions for both packing and unpacking U8 archives, as well as code to parse IMET headers. IMET headers are a header format used specifically for U8 archives containing the banner of a channel, as they store the localized name of the channel along with other banner metadata.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.archive.u8
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -11,6 +11,7 @@ from datetime import date
project = 'libWiiPy' project = 'libWiiPy'
copyright = f'{date.today().year}, NinjaCheetah & Contributors' copyright = f'{date.today().year}, NinjaCheetah & Contributors'
author = 'NinjaCheetah & Contributors' author = 'NinjaCheetah & Contributors'
version = 'main'
release = 'main' release = 'main'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------

View File

@@ -4,9 +4,11 @@ sd_hide_title: true
# Overview # Overview
# libWiiPy API Docs # libWiiPy Documentation
Welcome to the API documentation website for libWiiPy! libWiiPy is a modern Python 3 library for handling the various files and formats found on the Wii. Welcome to the documentation website for libWiiPy! libWiiPy is a modern Python 3 library for handling the various files and formats found on the Wii.
Just need to see the API? [libWiiPy API Documentation](/api)
```{toctree} ```{toctree}
:hidden: :hidden:
@@ -34,13 +36,12 @@ titles/nus-downloading.md
```{toctree} ```{toctree}
:hidden: :hidden:
:caption: Other Useful Pages :caption: More
modules.md api.md
``` ```
## Indices and tables ## Indices and tables
* [Full Index](<project:#genindex>) * [Full Index](<project:#genindex>)
* [Module Index](<project:#modules>)
* <project:#search> * <project:#search>

View File

@@ -1,28 +0,0 @@
# libWiiPy.archive package
## Submodules
### libWiiPy.archive.ash module
```{eval-rst}
.. automodule:: libWiiPy.archive.ash
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.archive.u8 module
```{eval-rst}
.. automodule:: libWiiPy.archive.u8
:members:
:undoc-members:
:show-inheritance:
```
## Module contents
```{eval-rst}
.. automodule:: libWiiPy.archive
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -1,36 +0,0 @@
# libWiiPy package
## Subpackages
```{toctree}
:maxdepth: 4
libWiiPy.archive
libWiiPy.media
libWiiPy.nand
libWiiPy.title
```
## Submodules
### libWiiPy.shared module
libWiiPy's ``shared`` module is private and contains only private functions used by other modules.
```{eval-rst}
.. automodule:: libWiiPy.shared
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.types module
libWiiPy's ``types`` module is private and contains only private classes used by other modules.
```{eval-rst}
.. automodule:: libWiiPy.types
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -1,11 +0,0 @@
# libWiiPy.media package
## Submodules
### libWiiPy.media.banner module
```{eval-rst}
.. automodule:: libWiiPy.media.banner
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -1,27 +0,0 @@
# libWiiPy.nand package
## Submodules
### libWiiPy.nand.emunand module
```{eval-rst}
.. automodule:: libWiiPy.nand.emunand
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.nand.setting module
```{eval-rst}
.. automodule:: libWiiPy.nand.setting
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.nand.sys module
```{eval-rst}
.. automodule:: libWiiPy.nand.sys
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -1,91 +0,0 @@
# libWiiPy.title package
## Submodules
### libWiiPy.title.commonkeys module
```{eval-rst}
.. automodule:: libWiiPy.title.commonkeys
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.title.content module
```{eval-rst}
.. automodule:: libWiiPy.title.content
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.title.crypto module
```{eval-rst}
.. automodule:: libWiiPy.title.crypto
:members:
:undoc-members:
:show-inheritance:
```
### libWiipy.title.iospatcher module
```{eval-rst}
.. automodule:: libWiiPy.title.iospatcher
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.title.nus module
```{eval-rst}
.. automodule:: libWiiPy.title.nus
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.title.ticket module
```{eval-rst}
.. automodule:: libWiiPy.title.ticket
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.title.title module
```{eval-rst}
.. automodule:: libWiiPy.title.title
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.title.tmd module
```{eval-rst}
.. automodule:: libWiiPy.title.tmd
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.title.util module
```{eval-rst}
.. automodule:: libWiiPy.title.util
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.title.wad module
```{eval-rst}
.. automodule:: libWiiPy.title.wad
:members:
:undoc-members:
:show-inheritance:
```
## Module contents
```{eval-rst}
.. automodule:: libWiiPy.title
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -0,0 +1,12 @@
# libWiiPy.media.banner Module
The `libWiiPy.media.banner` module is essentially a stub at this point in time. It only provides one dataclass that is likely to become a traditional class when fully implemented. It is not recommended to use this module for anything yet.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.media.banner
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -0,0 +1,16 @@
# libWiiPy.media Package
## Modules
The `libWiiPy.media` package contains modules used for parsing and editing media formats used by the Wii. This currently only includes limited support for parsing channel banners.
| Module | Description |
|----------------------------------------|---------------------------------------------------|
| [libWiiPy.media.banner](/media/banner) | Provides support for basic channel banner parsing |
### libWiiPy.media Package Contents
```{toctree}
:maxdepth: 4
/media/banner
```

View File

@@ -1,7 +0,0 @@
# Modules Overview
```{toctree}
:maxdepth: 4
libWiiPy
```

View File

@@ -0,0 +1,12 @@
# libWiiPy.nand.emunand Module
The `libWiiPy.nand.emunand` module provides support for creating and managing Wii EmuNANDs. At present, you cannot create an EmuNAND compatible with something like NEEK on a real Wii with the features provided by this library, but you can create an EmuNAND compatible with Dolphin.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.nand.emunand
:members:
:undoc-members:
:show-inheritance:
```

20
docs/source/nand/nand.md Normal file
View File

@@ -0,0 +1,20 @@
# libWiiPy.nand Package
## Modules
The `libWiiPy.nand` package contains modules for parsing and manipulating EmuNANDs as well as modules for parsing and editing core system files found on the Wii's NAND.
| Module | Description |
|----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
| [libWiiPy.nand.emunand](/nand/emunand) | Provides support for parsing, creating, and editing EmuNANDs |
| [libWiiPy.nand.setting](/nand/setting) | Provides support for parsing, creating, and editing `setting.txt`, which is used to store the console's region and serial number |
| [libWiiPy.nand.sys](/nand/sys) | Provides support for parsing, creating, and editing `uid.sys`, which is used to store a log of all titles run on a console |
### libWiiPy.nand Package Contents
```{toctree}
:maxdepth: 4
/nand/emunand
/nand/setting
/nand/sys
```

View File

@@ -0,0 +1,14 @@
# libWiiPy.nand.setting Module
The `libWiiPy.nand.setting` module provides support for handling the Wii's `setting.txt` file. This file is stored as part of the Wii Menu's save data (stored in `/title/00000001/00000002/data/`) and is an encrypted text file that's primarily used to store your console's serial number and region information.
This module allows you to encrypt or decrypt this file, and exposes the keys stored in it for editing.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.nand.setting
:members:
:undoc-members:
:show-inheritance:
```

12
docs/source/nand/sys.md Normal file
View File

@@ -0,0 +1,12 @@
# libWiiPy.nand.sys Module
The `libWiiPy.nand.sys` module provides support for editing system files used on the Wii. Currently, it only offers support for `uid.sys`, which keeps a record of the Title IDs of every title launched on the console, assigning each one a unique ID.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.nand.sys
:members:
:undoc-members:
:show-inheritance:
```

14
docs/source/title/cert.md Normal file
View File

@@ -0,0 +1,14 @@
# libWiiPy.title.cert Module
The `libWiiPy.title.cert` module provides support for parsing the various signing certificates used by the Wii for content validation.
This module allows you to write your own code for validating the authenticity of a TMD or Ticket by providing the certificates from the Wii's certificate chain. Both retail and development certificate chains are supported.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.cert
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -0,0 +1,12 @@
# libWiiPy.title.commonkeys Module
The `libWiiPy.title.commonkeys` module simply provides easy access to the Wii's common encryption keys.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.commonkeys
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -0,0 +1,12 @@
# libWiiPy.title.content Module
The `libWiiPy.title.content` module provides support for parsing, adding, removing, and editing content files from a digital Wii title.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.content
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -0,0 +1,12 @@
# libWiiPy.title.crypto Module
The `libWiiPy.title.crypto` module provides low-level cryptography functions required for handling digital Wii titles. It does not expose many functions that are likely to be required during typical use, and instead acts more as a dependency for other modules.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.crypto
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -0,0 +1,12 @@
# libWiiPy.title.iospatcher Module
The `libWiiPy.title.iospatcher` module provides support for applying various binary patches to IOS' ES module. These patches and what they do can be found attached to the methods used to apply them.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.iospatcher
:members:
:undoc-members:
:show-inheritance:
```

12
docs/source/title/nus.md Normal file
View File

@@ -0,0 +1,12 @@
# libWiiPy.title.nus Module
The `libWiiPy.title.nus` module provides support for downloading digital Wii titles from the Nintendo Update Servers. This module provides easy methods for downloading TMDs, common Tickets (when present), encrypted content, and the certificate chain.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.nus
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -0,0 +1,12 @@
# libWiiPy.title.ticket Module
The `libWiiPy.title.ticket` module provides support for handling Tickets, which are the license files used to decrypt the content of digital titles during installation. This module allows for easy parsing and editing of Tickets.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.ticket
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -0,0 +1,36 @@
# libWiiPy.title Package
## Modules
The `libWiiPy.title` package contains modules for interacting with Wii titles. This is the most complete package in libWiiPy, and therefore offers the most functionality.
| Module | Description |
|------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| [libWiiPy.title.cert](/title/cert) | Provides support for parsing and validating the certificates used for title verification |
| [libWiiPy.title.commonkeys](/title/commonkeys) | Provides easy access to all common encryption keys |
| [libWiiPy.title.content](/title/content) | Provides support for parsing and editing content included as part of digital titles |
| [libWiiPy.title.crypto](/title/crypto) | Provides low-level cryptography functions used to handle encryption in other modules |
| [libWiiPy.title.iospatcher](/title/iospatcher) | Provides an easy interface to apply patches to IOSes |
| [libWiiPy.title.nus](/title/nus) | Provides support for downloading TMDs, Tickets, encrypted content, and the certificate chain from the Nintendo Update Servers |
| [libWiiPy.title.ticket](/title/ticket) | Provides support for parsing and editing Tickets used for content decryption |
| [libWiiPy.title.title](/title/title.title) | Provides high-level support for parsing and editing an entire title with the context of each component |
| [libWiiPy.title.tmd](/title/tmd) | Provides support for parsing and editing TMDs (Title Metadata) |
| [libWiiPy.title.util](/title/util) | Provides some simple utility functions relating to titles |
| [libWiiPy.title.wad](/title/wad) | Provides support for parsing and editing WAD files, allowing you to load each component into the other available classes |
### libWiiPy.title Package Contents
```{toctree}
:maxdepth: 4
/title/cert
/title/commonkeys
/title/content
/title/crypto
/title/iospatcher
/title/nus
/title/ticket
/title/title.title
/title/tmd
/title/util
/title/wad
```

View File

@@ -0,0 +1,16 @@
# libWiiPy.title.title Module
The `libWiiPy.title.title` module provides a high-level interface for handling all the components of a digital Wii title through one class. It allows for directly importing a WAD, and will automatically extract the various components and load them into their appropriate classes. Additionally, it provides duplicates of some methods found in those classes that require fewer arguments, as it has the context of the other components and is able to retrieve additional data automatically.
An example of that idea can be seen with the method `get_content_by_index()`. In its original definition, which can be seen at <project:#libWiiPy.title.content.ContentRegion.get_content_by_index>, you are required to supply the Title Key for the title that the content is sourced from. In contrast, when using <project:#libWiiPy.title.title.Title.get_content_by_index>, you do not need to supply a Title Key, as the Title object already has the context of the Ticket and can retrieve the Title Key from it automatically. In a similar vein, this module provides the easiest route for verifying that a title is legitimately signed by Nintendo. The method <project:#libWiiPy.title.title.Title.get_is_signed> is able to access the entire certificate chain, the TMD, and the Ticket, and is therefore able to verify all components of the title by itself.
Because using <project:#libWiiPy.title.title.Title> allows many operations to be much simpler than if you manage the components separately, it's generally recommended to use it whenever possible.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.title
:members:
:undoc-members:
:show-inheritance:
```

12
docs/source/title/tmd.md Normal file
View File

@@ -0,0 +1,12 @@
# libWiiPy.title.tmd Module
The `libWiiPy.title.tmd` module provides support for handling TMD (Title Metadata) files, which contain the metadata of both digital and physical Wii titles. This module allows for easy parsing and editing of TMDs.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.tmd
:members:
:undoc-members:
:show-inheritance:
```

12
docs/source/title/util.md Normal file
View File

@@ -0,0 +1,12 @@
# libWiiPy.title.util Module
The `libWiiPy.title.util` module provides common utility functions internally. It is not designed to be used directly.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.util
:members:
:undoc-members:
:show-inheritance:
```

12
docs/source/title/wad.md Normal file
View File

@@ -0,0 +1,12 @@
# libWiiPy.title.wad Module
The `libWiiPy.title.wad` module provides support for handling WAD (Wii Archive Data) files, which is the format used to deliver digital Wii titles. This module allows for extracting the various components for a WAD, as well as properly padding and writing out that data when it has been edited using other modules.
## Module Contents
```{eval-rst}
.. automodule:: libWiiPy.title.wad
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -34,7 +34,7 @@ And viola! We have a WAD object that we can use to get each separate part of our
## Picking the WAD Apart ## Picking the WAD Apart
Now that we have our WAD loaded, we need to separate it out into its components. On top of the parts we already established, a WAD also contains a certificate, checked by IOS during official title installations to ensure that a title was signed by Nintendo, and potentially two more areas called the footer and the CRL. Footers aren't a necessary part of a WAD, and when they do exist, they typically only contain the build timestamp and the machine it was built on. CRLs are even less common, and have never actually been found inside any WAD, but we know they exist because of things we've seen that Nintendo would really rather we hadn't. Because these three components don't have data we can edit, they're only ever represented as bytes, and do not have their own classes. Now that we have our WAD loaded, we need to separate it out into its components. On top of the parts we already established, a WAD also contains a certificate chain, which is used by IOS during official title installations to ensure that a title was signed by Nintendo, and potentially two more areas called the footer and the CRL. Footers aren't a necessary part of a WAD, and when they do exist, they typically only contain the build timestamp and the machine it was built on. CRLs are even less common, and have never actually been found inside any WAD, but we know they exist because of things we've seen that Nintendo would really rather we hadn't. Certificate chains also have a class that we'll cover after the main three components, but the latter two components don't have data we can edit, so they're only ever represented as bytes and do not have their own classes.
### The TMD ### The TMD
@@ -110,11 +110,19 @@ Now that we know things are working, why don't we speed things up a little by us
And just like that, we have our TMD, Ticket, and decrypted content all extracted! From here, what you do with them is up to you and whatever program you're working on. For example, to make a simple WAD extractor, you may want to write all these files to an output directory. And just like that, we have our TMD, Ticket, and decrypted content all extracted! From here, what you do with them is up to you and whatever program you're working on. For example, to make a simple WAD extractor, you may want to write all these files to an output directory.
### The Certificate Chain
As mentioned at the start of this guide, WADs also contain a certificate chain. We don't necessarily need this data right now, but getting it is very similar to the other components:
```pycon
>>> certificate_chain = libWiiPy.title.CertificateChain()
>>> certificate_chain.load(wad.get_cert_data())
>>>
```
### The Other Data ### The Other Data
As mentioned earlier in this guide, WADs also contain up to three extra regions of data: the certificate, the footer, and the CRL. The procedure for extracting all of these is pretty simple, and follows the same formula as any other data in a WAD: Also mentioned earlier in this guide, WADs may contain two additional regions of data know as the footer (or "meta"), and the CRL. The procedure for extracting all of these is pretty simple, and follows the same formula as any other data in a WAD:
```pycon ```pycon
>>> certificate = wad.get_cert_data()
>>> footer = wad.get_meta_data() >>> footer = wad.get_meta_data()
>>> crl = wad.get_crl_data() >>> crl = wad.get_crl_data()
>>> >>>
@@ -123,7 +131,7 @@ As mentioned earlier in this guide, WADs also contain up to three extra regions
Beyond getting their raw data, there isn't anything you can directly do with these components with libWiiPy. If one of these components doesn't exist, libWiiPy will simply return an empty bytes object. Beyond getting their raw data, there isn't anything you can directly do with these components with libWiiPy. If one of these components doesn't exist, libWiiPy will simply return an empty bytes object.
:::{note} :::{note}
Managed to find a WAD somewhere with CRL data? I'd love to here more, so feel free to email me at [ninjacheetah@ncxprogramming.com](mailto:ninjacheetah@ncxprogramming.com). Managed to find a WAD somewhere with CRL data? I'd love to hear more, so feel free to email me at [ninjacheetah@ncxprogramming.com](mailto:ninjacheetah@ncxprogramming.com).
::: :::
<hr> <hr>

View File

@@ -2,4 +2,5 @@
# https://github.com/NinjaCheetah/libWiiPy # https://github.com/NinjaCheetah/libWiiPy
from .ash import * from .ash import *
from .lz77 import *
from .u8 import * from .u8 import *

View File

@@ -5,7 +5,7 @@
# co-authored by NinjaCheetah. # co-authored by NinjaCheetah.
# https://github.com/NinjaCheetah/ASH0-tools # https://github.com/NinjaCheetah/ASH0-tools
# #
# See <link pending> for details about the ASH archive format. # See <link pending> for details about the ASH compression format.
import io import io
from dataclasses import dataclass as _dataclass from dataclasses import dataclass as _dataclass

View File

@@ -0,0 +1,211 @@
# "archive/lz77.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy
#
# See https://wiibrew.org/wiki/LZ77 for details about the LZ77 compression format.
import io
from dataclasses import dataclass as _dataclass
_LZ_MIN_DISTANCE = 0x01 # Minimum distance for each reference.
_LZ_MAX_DISTANCE = 0x1000 # Maximum distance for each reference.
_LZ_MIN_LENGTH = 0x03 # Minimum length for each reference.
_LZ_MAX_LENGTH = 0x12 # Maximum length for each reference.
@_dataclass
class _LZNode:
dist: int = 0
len: int = 0
weight: int = 0
def _compress_compare_bytes(byte1: bytes, offset1: int, byte2: bytes, offset2: int, abs_len_max: int) -> int:
# Compare bytes up to the maximum length we can match.
num_matched = 0
while num_matched < abs_len_max:
if byte1[offset1 + num_matched] != byte2[offset2 + num_matched]:
break
num_matched += 1
return num_matched
def _compress_search_matches(buffer: bytes, pos: int) -> (int, int):
bytes_left = len(buffer) - pos
global _LZ_MAX_DISTANCE, _LZ_MAX_LENGTH, _LZ_MIN_DISTANCE
# Default to only looking back 4096 bytes, unless we've moved fewer than 4096 bytes, in which case we should
# only look as far back as we've gone.
max_dist = min(_LZ_MAX_DISTANCE, pos)
# Default to only matching up to 18 bytes, unless fewer than 18 bytes remain, in which case we can only match
# up to that many bytes.
max_len = min(_LZ_MAX_LENGTH, bytes_left)
# Log the longest match we found and its offset.
biggest_match, biggest_match_pos = 0, 0
# Search for matches.
for i in range(_LZ_MIN_DISTANCE, max_dist + 1):
num_matched = _compress_compare_bytes(buffer, pos - i, buffer, pos, max_len)
if num_matched > biggest_match:
biggest_match = num_matched
biggest_match_pos = i
if biggest_match == max_len:
break
return biggest_match, biggest_match_pos
def _compress_node_is_ref(node: _LZNode) -> bool:
return node.len >= _LZ_MIN_LENGTH
def _compress_get_node_cost(length: int) -> int:
if length >= _LZ_MIN_LENGTH:
num_bytes = 2
else:
num_bytes = 1
return 1 + (num_bytes * 8)
def compress_lz77(data: bytes) -> bytes:
"""
Compresses data using the Wii's LZ77 compression algorithm and returns the compressed result.
Parameters
----------
data: bytes
The data to compress.
Returns
-------
bytes
The LZ77-compressed data.
"""
nodes = [_LZNode() for _ in range(len(data))]
# Iterate over the uncompressed data, starting from the end.
pos = len(data)
global _LZ_MAX_LENGTH, _LZ_MIN_LENGTH, _LZ_MIN_DISTANCE
while pos:
pos -= 1
node = nodes[pos]
# Limit the maximum search length when we're near the end of the file.
max_search_len = min(_LZ_MAX_LENGTH, len(data) - pos)
if max_search_len < _LZ_MIN_DISTANCE:
max_search_len = 1
# Initialize as 1 for each, since that's all we could use if we weren't compressing.
length, dist = 1, 1
if max_search_len >= _LZ_MIN_LENGTH:
length, dist = _compress_search_matches(data, pos)
# Treat as direct bytes if it's too short to copy.
if length == 0 or length < _LZ_MIN_LENGTH:
length = 1
# If the node goes to the end of the file, the weight is the cost of the node.
if (pos + length) == len(data):
node.len = length
node.dist = dist
node.weight = _compress_get_node_cost(length)
# Otherwise, search for possible matches and determine the one with the best cost.
else:
weight_best = 0xFFFFFFFF # This was originally UINT_MAX, but that isn't a thing here so 32-bit it is!
len_best = 1
while length:
weight_next = nodes[pos + length].weight
weight = _compress_get_node_cost(length) + weight_next
if weight < weight_best:
len_best = length
weight_best = weight
length -= 1
if length != 0 and length < _LZ_MIN_LENGTH:
length = 1
node.len = len_best
node.dist = dist
node.weight = weight_best
# Write the header data.
with io.BytesIO() as buffer:
# Write the header data.
buffer.write(b'LZ77\x10') # The LZ type on the Wii is *always* 0x10.
buffer.write(len(data).to_bytes(3, 'little'))
src_pos = 0
while src_pos < len(data):
head = 0
head_pos = buffer.tell()
buffer.write(b'\x00') # Reserve a byte for the chunk head.
i = 0
while i < 8 and src_pos < len(data):
current_node = nodes[src_pos]
length = current_node.len
dist = current_node.dist
# This is a reference node.
if _compress_node_is_ref(current_node):
encoded = (((length - _LZ_MIN_LENGTH) & 0xF) << 12) | ((dist - _LZ_MIN_DISTANCE) & 0xFFF)
buffer.write(encoded.to_bytes(2))
head = (head | (1 << (7 - i))) & 0xFF
# This is a direct copy node.
else:
buffer.write(data[src_pos:src_pos + 1])
src_pos += length
i += 1
pos = buffer.tell()
buffer.seek(head_pos)
buffer.write(head.to_bytes(1))
buffer.seek(pos)
buffer.seek(0)
out_data = buffer.read()
return out_data
def decompress_lz77(lz77_data: bytes) -> bytes:
"""
Decompresses LZ77-compressed data and returns the decompressed result. Supports data both with and without the
magic number 'LZ77' (which may not be present if the data is embedded in something else).
Parameters
----------
lz77_data: bytes
The LZ77-compressed data to decompress.
Returns
-------
bytes
The decompressed data.
"""
with io.BytesIO(lz77_data) as data:
magic = data.read(4)
# Assume if we didn't get the magic number that this data starts without it.
if magic != b'LZ77':
data.seek(0)
# Other compression types are used by Nintendo, but only type 0x10 was used on the Wii.
compression_type = int.from_bytes(data.read(1))
if compression_type != 0x10:
raise ValueError("This data is using an unsupported compression type!")
decompressed_size = int.from_bytes(data.read(3), byteorder='little')
# Use an integer list for storing decompressed data, this is much faster than using (and appending to) a
# bytes object.
out_data = [0] * decompressed_size
pos = 0
while pos < decompressed_size:
flag = int.from_bytes(data.read(1))
# Read bits in the flag from most to least significant.
for x in range(7, -1, -1):
# Avoids a buffer overrun if the final flag isn't fully used.
if pos >= decompressed_size:
break
# Result of 1, this means we're copying bytes from earlier in the data.
if flag & (1 << x):
reference = int.from_bytes(data.read(2))
length = 3 + ((reference >> 12) & 0xF)
offset = pos - (reference & 0xFFF) - 1
for _ in range(length):
out_data[pos] = out_data[offset]
pos += 1
offset += 1
# Avoids a buffer overrun if the copy length would extend past the end of the file.
if pos >= decompressed_size:
break
# Result of 0, use the next byte directly.
else:
out_data[pos] = int.from_bytes(data.read(1))
pos += 1
out_bytes = bytes(out_data)
return out_bytes

View File

@@ -199,8 +199,8 @@ class EmuNAND:
valid_lows = [] valid_lows = []
for low in tid_lows: for low in tid_lows:
if low.joinpath("content", "title.tmd").exists(): if low.joinpath("content", "title.tmd").exists():
valid_lows.append(low.name) valid_lows.append(low.name.upper())
installed_titles.append(self.InstalledTitles(high.name, valid_lows)) installed_titles.append(self.InstalledTitles(high.name.upper(), valid_lows))
return installed_titles return installed_titles
def get_title_tmd(self, tid: str) -> TMD: def get_title_tmd(self, tid: str) -> TMD:

View File

@@ -1,6 +1,7 @@
# "title/__init__.py" from libWiiPy by NinjaCheetah & Contributors # "title/__init__.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy # https://github.com/NinjaCheetah/libWiiPy
from .cert import *
from .content import * from .content import *
from .crypto import * from .crypto import *
from .iospatcher import * from .iospatcher import *

362
src/libWiiPy/title/cert.py Normal file
View File

@@ -0,0 +1,362 @@
# "title/cert.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy
#
# See https://wiibrew.org/wiki/Certificate_chain for details about the Wii's certificate chain
import io
from enum import IntEnum as _IntEnum
from ..shared import _align_value, _pad_bytes
from .ticket import Ticket
from .tmd import TMD
from Crypto.Hash import SHA1
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
class CertificateType(_IntEnum):
RSA_4096 = 0x00010000
RSA_2048 = 0x00010001
ECC = 0x00010002
class CertificateSignatureLength(_IntEnum):
RSA_4096 = 0x200
RSA_2048 = 0x100
ECC = 0x3C
class CertificateKeyType(_IntEnum):
RSA_4096 = 0x00000000
RSA_2048 = 0x00000001
ECC = 0x00000002
class CertificateKeyLength(_IntEnum):
RSA_4096 = 0x200
RSA_2048 = 0x100
ECC = 0x3C
class Certificate:
"""
A Certificate object used to parse a certificate used for the Wii's content verification.
Attributes
----------
type: CertificateType
The type of the certificate, either RSA-2048, RSA-4096, or ECC.
signature: bytes
The signature data of the certificate.
issuer: str
The certificate that issued this certificate.
pub_key_type: CertificateKeyType
The type of public key contained in the certificate, either RSA-2048, RSA-4096, or ECC.
child_name: str
The name of this certificate.
pub_key_id: int
The ID of this certificate's public key.
pub_key_modulus: int
The modulus of this certificate's public key. Combined with the exponent to get the full key.
pub_key_exponent: int
The exponent of this certificate's public key. Combined with the modulus to get the full key.
"""
def __init__(self):
self.type: CertificateType | None = None
self.signature: bytes = b''
self.issuer: str = ""
self.pub_key_type: CertificateKeyType | None = None
self.child_name: str = ""
self.pub_key_id: int = 0
self.pub_key_modulus: int = 0
self.pub_key_exponent: int = 0
def load(self, cert: bytes) -> None:
"""
Loads certificate data into the Certificate object, allowing you to parse the certificate.
Parameters
----------
cert: bytes
The data for the certificate to load.
"""
with io.BytesIO(cert) as cert_data:
# Read the first 4 bytes of the cert to get the certificate's type.
try:
self.type = CertificateType.from_bytes(cert_data.read(0x4))
except ValueError:
raise ValueError("Invalid Certificate Type!")
cert_length = CertificateSignatureLength[self.type.name]
self.signature = cert_data.read(cert_length.value)
cert_data.seek(0x40 + cert_length.value)
self.issuer = str(cert_data.read(0x40).replace(b'\x00', b'').decode())
try:
cert_data.seek(0x80 + cert_length.value)
self.pub_key_type = CertificateKeyType.from_bytes(cert_data.read(0x4))
except ValueError:
raise ValueError("Invalid Certificate Key type!")
cert_data.seek(0x84 + cert_length.value)
self.child_name = str(cert_data.read(0x40).replace(b'\x00', b'').decode())
cert_data.seek(0xC4 + cert_length.value)
self.pub_key_id = int.from_bytes(cert_data.read(0x4))
key_length = CertificateKeyLength[self.pub_key_type.name]
cert_data.seek(0xC8 + cert_length.value)
self.pub_key_modulus = int.from_bytes(cert_data.read(key_length.value))
if self.pub_key_type == CertificateKeyType.RSA_4096 or self.pub_key_type == CertificateKeyType.RSA_2048:
self.pub_key_exponent = int.from_bytes(cert_data.read(0x4))
def dump(self) -> bytes:
"""
Dump the certificate object back into bytes.
Returns
-------
bytes:
The certificate file as bytes.
"""
cert_data = b''
cert_data += int.to_bytes(self.type.value, 4)
cert_data += self.signature
cert_data = _pad_bytes(cert_data)
# Pad out the issuer name with null bytes.
issuer = self.issuer.encode()
while len(issuer) < 0x40:
issuer += b'\x00'
cert_data += issuer
cert_data += int.to_bytes(self.pub_key_type.value, 4)
# Pad out the child cert name with null bytes
child_name = self.child_name.encode()
while len(child_name) < 0x40:
child_name += b'\x00'
cert_data += child_name
cert_data += int.to_bytes(self.pub_key_id, 4)
cert_data += int.to_bytes(self.pub_key_modulus, CertificateKeyLength[self.pub_key_type.name])
if self.pub_key_type == CertificateKeyType.RSA_4096 or self.pub_key_type == CertificateKeyType.RSA_2048:
cert_data += int.to_bytes(self.pub_key_exponent, 4)
# Pad out the certificate data to a multiple of 64.
cert_data = _pad_bytes(cert_data)
return cert_data
class CertificateChain:
"""
A CertificateChain object used to parse the chain of certificates stored in a WAD that are used for the Wii's
content verification. The certificate chain is the format that the certificates are stored in as part of every WAD.
Attributes
----------
ca_cert: Certificate
The CA certificate from the chain.
tmd_cert: Certificate
The CP (TMD) certificate from the chain.
ticket_cert: Certificate
The XS (Ticket) certificate from the chain.
"""
def __init__(self):
self.ca_cert: Certificate = Certificate()
self.tmd_cert: Certificate = Certificate()
self.ticket_cert: Certificate = Certificate()
def load(self, cert_chain: bytes) -> None:
"""
Loads certificate chain data into the CertificateChain object, allowing you to parse the individual
certificates stored in the chain.
Parameters
----------
cert_chain: bytes
The data for the certificate chain to load.
"""
with (io.BytesIO(cert_chain) as cert_chain_data):
# Read the two fields that denote different length sections of the certificate, so that we know how long
# this certificate is in total.
offset = 0x0
for _ in range(3):
cert_chain_data.seek(offset)
cert_type = CertificateType.from_bytes(cert_chain_data.read(0x4))
cert_chain_data.seek(offset + 0x80 + CertificateSignatureLength[cert_type.name].value)
key_type = CertificateKeyType.from_bytes(cert_chain_data.read(0x4))
cert_size = _align_value(0xC8 + CertificateSignatureLength[cert_type.name].value +
CertificateKeyLength[key_type.name].value)
cert_chain_data.seek(offset + 0x0)
cert = Certificate()
cert.load(cert_chain_data.read(cert_size))
if cert.issuer == "Root":
self.ca_cert = cert
elif cert.issuer.find("Root-CA") != -1:
if cert.child_name.find("CP") != -1:
self.tmd_cert = cert
elif cert.child_name.find("XS") != -1:
self.ticket_cert = cert
else:
raise ValueError("Unknown certificate in chain!")
else:
raise ValueError("Unknown certificate in chain!")
offset += cert_size
def dump(self) -> bytes:
"""
Dumps the full certificate chain back into bytes. This chain will always be formatted with the CA cert first,
followed by the CP (TMD) cert, then finally the XS (Ticket) cert.
Returns
-------
bytes
The full certificate chain as bytes.
"""
cert_chain_data = b''
cert_chain_data += self.ca_cert.dump()
cert_chain_data += self.tmd_cert.dump()
cert_chain_data += self.ticket_cert.dump()
return cert_chain_data
def verify_ca_cert(ca_cert: Certificate) -> bool:
"""
Verify a Wii CA certificate using the root public key. The retail or development root key will be automatically
selected based off of the name of the CA certificate provided.
Parameters
----------
ca_cert: Certificate
The CA certificate to verify.
Returns
-------
bool
Whether the certificate is valid or not.
"""
if ca_cert.issuer != "Root" or ca_cert.child_name.find("CA") == -1:
raise ValueError("The provided certificate is not a CA certificate!")
if ca_cert.child_name == "CA00000001":
root_key_modulus = \
(b'\xf8$lX\xba\xe7P\x03\x01\xfb\xb7\xc2\xeb\xe0\x01\x05q\xda\x92#x\xf0QN\xc0\x03\x1d\xd0\xd2\x1e\xd3\xd0~'
b'\xfc\x85 i\xb5\xde\x9b\xb9Q\xa8\xbc\x90\xa2D\x92m7\x92\x95\xae\x946\xaa\xa6\xa3\x02Q\x0c{\x1d\xed\xd5'
b'\xfb \x86\x9d\x7f0\x16\xf6\xbee\xd3\x83\xa1m\xb32\x1b\x955\x18\x90\xb1p\x02\x93~\xe1\x93\xf5~\x99\xa2GN'
b'\x9d8$\xc7\xae\xe3\x85A\xf5g\xe7Q\x8cz\x0e8\xe7\xeb\xafA\x19\x1b\xcf\xf1{B\xa6\xb4\xed\xe6\xce\x8d\xe71'
b'\x8f\x7fR\x04\xb3\x99\x0e"gE\xaf\xd4\x85\xb2D\x93\x00\x8b\x08\xc7\xf6\xb7\xe5k\x02\xb3\xe8\xfe\x0c\x9d'
b'\x85\x9c\xb8\xb6\x82#\xb8\xab\'\xee_e8\x07\x8b-\xb9\x1e*\x15>\x85\x81\x80r\xa2;m\xd92\x81\x05Oo\xb0\xf6'
b'\xf5\xad(>\xca\x0bz\xf3TU\xe0=\xa7\xb6\x83&\xf3\xec\x83J\xf3\x14\x04\x8a\xc6\xdf \xd2\x85\x08g<\xabb\xa2'
b'\xc7\xbc\x13\x1aS>\x0bf\x80k\x1c0fK7#1\xbd\xc4\xb0\xca\xd8\xd1\x1e\xe7\xbb\xd9(UH\xaa\xec\x1ff\xe8!\xb3'
b'\xc8\xa0Gi\x00\xc5\xe6\x88\xe8\x0c\xce<a\xd6\x9c\xbb\xa17\xc6`Ozr\xdd\x8c{>=Q)\r\xaajY{\x08\x1f\x9d63'
b'\xa3Fz5a\t\xac\xa7\xdd}./\xb2\xc1\xae\xb8\xe2\x0fH\x92\xd8\xb9\xf8\xb4oN<\x11\xf4\xf4}\x8bu}\xfe\xfe\xa3'
b'\x89\x9c3Y\\^\xfd\xeb\xcb\xab\xe8A>:\x9a\x80<i5n\xb2\xb2\xad\\\xc4\xc8XE^\xf5\xf7\xb3\x06D\xb4|d\x06\x8c'
b'\xdf\x80\x9fv\x02Z-\xb4F\xe0=|\xf6/4\xe7\x02E{\x02\xa4\xcf]\x9d\xd5<\xa5:|\xa6)x\x8cg\xca\x08\xbf\xec'
b'\xcaC\xa9W\xad\x16\xc9N\x1c\xd8u\xca\x10}\xce~\x01\x18\xf0\xdfk\xfe\xe5\x1d\xdb\xd9\x91\xc2n`\xcdHX\xaa'
b'Y,\x82\x00u\xf2\x9fRl\x91|o\xe5@>\xa7\xd4\xa5\x0c\xec;s\x84\xde\x88n\x82\xd2\xebMNB\xb5\xf2\xb1I\xa8\x1e'
b'\xa7\xceqD\xdc)\x94\xcf\xc4N\x1f\x91\xcb\xd4\x95')
elif ca_cert.child_name == "CA00000002":
root_key_modulus = \
(b'\x00\xd0\x1f\xe1\x00\xd45V\xb2KV\xda\xe9q\xb5\xa5\xd3\x84\xb90\x03\xbe\x1b\xbf(\xa20[\x06\x06EF}[\x02Q'
b'\xd2V\x1a\'O\x9e\x9f\x9c\xecdaP\xab=*\xe36hf\xac\xa4\xba\xe8\x1a\xe3\xd7\x9a\xa6\xb0J\x8b\xcb\xa7\xe6'
b'\xfbd\x89E\xeb\xdf\xdb\x85\xba\t\x1f\xd7\xd1\x14\xb5\xa3\xa7\x80\xe3\xa2.n\xcd\x87\xb5\xa4\xc6\xf9\x10'
b'\xe4\x03"\x08\x81K\x0c\xee\xa1\xa1}\xf79i_a~\xf65(\xdb\x94\x967\xa0V\x03\x7f{2A8\x95\xc0\xa8\xf1\x98.'
b'\x15e\xe3\x8e\xed\xc2.Y\x0e\xe2g{\x86\t\xf4\x8c.0?\xbc@\\\xac\x18\x04/\x82 \x84\xe4\x93h\x03\xda\x7fA4'
b'\x92HV+\x8e\xe1/x\xf8\x03$c0\xbc{\xe7\xeerJ\xf4X\xa4r\xe7\xabF\xa1\xa7\xc1\x0c/\x18\xfa\x07\xc3\xdd\xd8'
b'\x98\x06\xa1\x1c\x9c\xc10\xb2G\xa3<\x8dG\xdeg\xf2\x9eUw\xb1\x1cCI=[\xbav4\xa7\xe4\xe7\x151\xb7\xdfY\x81'
b'\xfe$\xa1\x14UL\xbd\x8f\x00\\\xe1\xdb5\x08\\\xcf\xc7x\x06\xb6\xde%@h\xa2l\xb5I-E\x80C\x8f\xe1\xe5\xa9'
b'\xedu\xc5\xedE\x1d\xcex\x949\xcc\xc3\xba(\xa21*\x1b\x87\x19\xef\x0fs\xb7\x13\x95\x0c\x02Y\x1atb\xa6\x07'
b'\xf3|\n\xa7\xa1\x8f\xa9C\xa3mu*_A\x92\xf0\x13a\x00\xaa\x9c\xb4\x1b\xbe\x14\xbe\xb1\xf9\xfci/\xdf\xa0\x94'
b'F\xdeZ\x9d\xde,\xa5\xf6\x8c\x1c\x0c!B\x92\x87\xcb-\xaa\xa3\xd2cu/s\xe0\x9f\xafDy\xd2\x81t)\xf6\x98\x00'
b'\xaf\xdekY-\xc1\x98\x82\xbd\xf5\x81\xcc\xab\xf2\xcb\x91\x02\x9e\xf3\\L\xfd\xbb\xffI\xc1\xfa\x1b/\xe3\x1d'
b'\xe7\xa5`\xec\xb4~\xbc\xfe2B[\x95o\x81\xb6\x99\x17H~;x\x91Q\xdb.x\xb1\xfd.\xbe~bk>\xa1e\xb4\xfb\x00\xcc'
b'\xb7Q\xafPs)\xc4\xa3\x93\x9e\xa6\xdd\x9cP\xa0\xe78k\x01EykA\xafa\xf7\x85U\x94O;\xc2-\xc3\xbd\r\x00\xf8y'
b'\x8aB\xb1\xaa\xa0\x83 e\x9a\xc79Z\xb4\xf3)')
else:
raise ValueError("The provided CA certificate is not valid!")
root_key_exponent = 0x00010001
cert_hash = SHA1.new(ca_cert.dump()[576:])
public_key = RSA.construct((int.from_bytes(root_key_modulus), root_key_exponent))
try:
pkcs1_15.new(public_key).verify(cert_hash, ca_cert.signature)
return True
except ValueError:
return False
def verify_cert_sig(ca_cert: Certificate, target_cert: Certificate) -> bool:
"""
Verify a TMD or Ticket certificate using a CA certificate.
Parameters
----------
ca_cert: Certificate
The CA certificate to use for verification.
target_cert: Certificate
The target certificate to verify.
Returns
-------
bool
Whether the certificate's signature is valid or not.
"""
if ca_cert.issuer != "Root" or ca_cert.child_name.find("CA") == -1:
raise ValueError("The provided certificate is not a CA certificate!")
# The issuer of the TMD/Ticket certs is Root-CA0000000X, so prepend "Root-" to the CA cert child name. If these
# don't match, then there's probably a mismatch between retail and development certs.
if f"Root-{ca_cert.child_name}" != target_cert.issuer:
raise ValueError("The certificate you are trying to verify does not match the provided CA certificate!")
cert_hash = SHA1.new(target_cert.dump()[320:])
public_key = RSA.construct((ca_cert.pub_key_modulus, ca_cert.pub_key_exponent))
try:
pkcs1_15.new(public_key).verify(cert_hash, target_cert.signature)
return True
except ValueError:
return False
def verify_tmd_sig(tmd_cert: Certificate, tmd: TMD) -> bool:
"""
Verify the signature of a TMD file using a TMD certificate.
Parameters
----------
tmd_cert: Certificate
The TMD certificate to use for verification.
tmd: TMD
The TMD to verify.
Returns
-------
bool
Whether the TMD's signature is valid or not.
"""
if tmd_cert.issuer.find("Root-CA") == -1 or tmd_cert.child_name.find("CP") == -1:
raise ValueError("The provided TMD certificate is not valid!")
if f"{tmd_cert.issuer}-{tmd_cert.child_name}" != tmd.signature_issuer:
raise ValueError("The signature you are trying to verify was not created with the provided TMD certificate!")
tmd_hash = SHA1.new(tmd.dump()[320:])
public_key = RSA.construct((tmd_cert.pub_key_modulus, tmd_cert.pub_key_exponent))
try:
pkcs1_15.new(public_key).verify(tmd_hash, tmd.signature)
return True
except ValueError:
return False
def verify_ticket_sig(ticket_cert: Certificate, ticket: Ticket) -> bool:
"""
Verify the signature of a Ticket file using a Ticket certificate.
Parameters
----------
ticket_cert: Certificate
The Ticket certificate to use for verification.
ticket: Ticket
The Ticket to verify.
Returns
-------
bool
Whether the Ticket's signature is valid or not.
"""
if ticket_cert.issuer.find("Root-CA") == -1 or ticket_cert.child_name.find("XS") == -1:
raise ValueError("The provided Ticket certificate is not valid!")
if f"{ticket_cert.issuer}-{ticket_cert.child_name}" != ticket.signature_issuer:
raise ValueError("The signature you are trying to verify was not created with the provided Ticket certificate!")
ticket_hash = SHA1.new(ticket.dump()[320:])
public_key = RSA.construct((ticket_cert.pub_key_modulus, ticket_cert.pub_key_exponent))
try:
pkcs1_15.new(public_key).verify(ticket_hash, ticket.signature)
return True
except ValueError:
return False

View File

@@ -1,8 +1,8 @@
# "title/crypto.py" from libWiiPy by NinjaCheetah & Contributors # "title/crypto.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy # https://github.com/NinjaCheetah/libWiiPy
import struct
import binascii import binascii
import struct
from .commonkeys import get_common_key from .commonkeys import get_common_key
from Crypto.Cipher import AES as _AES from Crypto.Cipher import AES as _AES

View File

@@ -40,10 +40,10 @@ def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool
""" """
# First, create the new title. # First, create the new title.
title = Title() title = Title()
# Download and load the TMD, Ticket, and certs. # Download and load the certificate chain, TMD, and Ticket.
title.load_cert_chain(download_cert_chain(wiiu_endpoint, endpoint_override))
title.load_tmd(download_tmd(title_id, title_version, wiiu_endpoint, endpoint_override)) title.load_tmd(download_tmd(title_id, title_version, wiiu_endpoint, endpoint_override))
title.load_ticket(download_ticket(title_id, wiiu_endpoint, endpoint_override)) title.load_ticket(download_ticket(title_id, wiiu_endpoint, endpoint_override))
title.wad.set_cert_data(download_cert(wiiu_endpoint, endpoint_override))
# Download all contents # Download all contents
title.load_content_records() title.load_content_records()
title.content.content_list = download_contents(title_id, title.tmd, wiiu_endpoint, endpoint_override) title.content.content_list = download_contents(title_id, title.tmd, wiiu_endpoint, endpoint_override)
@@ -88,7 +88,14 @@ def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool =
if title_version is not None: if title_version is not None:
tmd_url += "." + str(title_version) tmd_url += "." + str(title_version)
# Make the request. # Make the request.
tmd_request = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True) try:
tmd_request = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
except requests.exceptions.ConnectionError:
if endpoint_override:
raise ValueError("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
"override is valid.")
else:
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 tmd_request.status_code != 200: if tmd_request.status_code != 200:
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"
@@ -133,7 +140,14 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_overrid
endpoint_url = _nus_endpoint[0] endpoint_url = _nus_endpoint[0]
ticket_url = endpoint_url + title_id + "/cetk" ticket_url = endpoint_url + title_id + "/cetk"
# Make the request. # Make the request.
ticket_request = requests.get(url=ticket_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True) try:
ticket_request = requests.get(url=ticket_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
except requests.exceptions.ConnectionError:
if endpoint_override:
raise ValueError("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
"override is valid.")
else:
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
if ticket_request.status_code != 200: if ticket_request.status_code != 200:
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.")
@@ -146,9 +160,9 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_overrid
return ticket return ticket
def download_cert(wiiu_endpoint: bool = False, endpoint_override: str = None) -> bytes: def download_cert_chain(wiiu_endpoint: bool = False, endpoint_override: str = None) -> bytes:
""" """
Downloads the signing certificate used by all WADs. This uses System Menu 4.3U as the source. Downloads the signing certificate chain used by all WADs. This uses System Menu 4.3U as the source.
Parameters Parameters
---------- ----------
@@ -163,7 +177,7 @@ def download_cert(wiiu_endpoint: bool = False, endpoint_override: str = None) ->
bytes bytes
The cert file. The cert file.
""" """
# Download the TMD and cetk for the System Menu 4.3U. # Download the TMD and cetk for System Menu 4.3U (v513).
if endpoint_override is not None: if endpoint_override is not None:
endpoint_url = _validate_endpoint(endpoint_override) endpoint_url = _validate_endpoint(endpoint_override)
else: else:
@@ -173,20 +187,28 @@ def download_cert(wiiu_endpoint: bool = False, endpoint_override: str = None) ->
endpoint_url = _nus_endpoint[0] endpoint_url = _nus_endpoint[0]
tmd_url = endpoint_url + "0000000100000002/tmd.513" tmd_url = endpoint_url + "0000000100000002/tmd.513"
cetk_url = endpoint_url + "0000000100000002/cetk" cetk_url = endpoint_url + "0000000100000002/cetk"
tmd = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content try:
cetk = requests.get(url=cetk_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content tmd = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content
# Assemble the certificate. cetk = requests.get(url=cetk_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content
cert = b'' except requests.exceptions.ConnectionError:
if endpoint_override:
raise ValueError("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
"override is valid.")
else:
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
# Assemble the certificate chain.
cert_chain = b''
# Certificate Authority data. # Certificate Authority data.
cert += cetk[0x2A4 + 768:] cert_chain += cetk[0x2A4 + 768:]
# Certificate Policy data. # Certificate Policy (TMD certificate) data.
cert += tmd[0x328:0x328 + 768] cert_chain += tmd[0x328:0x328 + 768]
# XS data. # XS (Ticket certificate) data.
cert += cetk[0x2A4:0x2A4 + 768] cert_chain += cetk[0x2A4:0x2A4 + 768]
# Since the cert is always the same, check the hash to make sure nothing went wildly wrong. # Since the cert chain is always the same, check the hash to make sure nothing went wildly wrong.
if hashlib.sha1(cert).hexdigest() != "ace0f15d2a851c383fe4657afc3840d6ffe30ad0": # This is currently disabled because of the possibility that one may be downloading non-retail certs (gasp!).
raise Exception("An unknown error has occurred downloading and creating the certificate.") #if hashlib.sha1(cert_chain).hexdigest() != "ace0f15d2a851c383fe4657afc3840d6ffe30ad0":
return cert # raise Exception("An unknown error has occurred downloading and creating the certificate.")
return cert_chain
def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False, def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False,
@@ -224,7 +246,14 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
endpoint_url = _nus_endpoint[0] endpoint_url = _nus_endpoint[0]
content_url = endpoint_url + title_id + "/000000" + content_id_hex content_url = endpoint_url + title_id + "/000000" + content_id_hex
# Make the request. # Make the request.
content_request = requests.get(url=content_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True) try:
content_request = requests.get(url=content_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
except requests.exceptions.ConnectionError:
if endpoint_override:
raise ValueError("A connection could not be made to the NUS endpoint. Please make sure that your endpoint "
"override is valid.")
else:
raise Exception("A connection could not be made to the NUS endpoint. The NUS may be unavailable.")
if content_request.status_code != 200: if content_request.status_code != 200:
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" +

View File

@@ -107,7 +107,7 @@ class Ticket:
self.signature = ticket_data.read(256) self.signature = ticket_data.read(256)
# Signature issuer. # Signature issuer.
ticket_data.seek(0x140) ticket_data.seek(0x140)
self.signature_issuer = str(ticket_data.read(64).decode()) self.signature_issuer = str(ticket_data.read(64).replace(b'\x00', b'').decode())
# ECDH data. # ECDH data.
ticket_data.seek(0x180) ticket_data.seek(0x180)
self.ecdh_data = ticket_data.read(60) self.ecdh_data = ticket_data.read(60)
@@ -160,7 +160,8 @@ class Ticket:
limit_value = int.from_bytes(ticket_data.read(4)) limit_value = int.from_bytes(ticket_data.read(4))
self.title_limits_list.append(_TitleLimit(limit_type, limit_value)) self.title_limits_list.append(_TitleLimit(limit_type, limit_value))
# Check certs to see if this is a retail or dev ticket. Treats unknown certs as being retail for now. # Check certs to see if this is a retail or dev ticket. Treats unknown certs as being retail for now.
if self.signature_issuer.find("Root-CA00000002-XS00000006") != -1: if (self.signature_issuer.find("Root-CA00000002-XS00000006") != -1 or
self.signature_issuer.find("Root-CA00000002-XS00000004") != -1):
self.is_dev = True self.is_dev = True
else: else:
self.is_dev = False self.is_dev = False
@@ -182,7 +183,10 @@ class Ticket:
# Padding to 64 bytes. # Padding to 64 bytes.
ticket_data += b'\x00' * 60 ticket_data += b'\x00' * 60
# Signature issuer. # Signature issuer.
ticket_data += str.encode(self.signature_issuer) signature_issuer = self.signature_issuer.encode()
while len(signature_issuer) < 0x40:
signature_issuer += b'\x00'
ticket_data += signature_issuer
# ECDH data. # ECDH data.
ticket_data += self.ecdh_data ticket_data += self.ecdh_data
# Ticket version. # Ticket version.

View File

@@ -4,10 +4,13 @@
# See https://wiibrew.org/wiki/Title for details about how titles are formatted # See https://wiibrew.org/wiki/Title for details about how titles are formatted
import math import math
from .content import ContentRegion from .cert import (CertificateChain as _CertificateChain,
from .ticket import Ticket verify_ca_cert as _verify_ca_cert, verify_cert_sig as _verify_cert_sig,
from .tmd import TMD verify_tmd_sig as _verify_tmd_sig, verify_ticket_sig as _verify_ticket_sig)
from .wad import WAD from .content import ContentRegion as _ContentRegion
from .ticket import Ticket as _Ticket
from .tmd import TMD as _TMD
from .wad import WAD as _WAD
from .crypto import encrypt_title_key from .crypto import encrypt_title_key
@@ -19,20 +22,23 @@ class Title:
Attributes Attributes
---------- ----------
wad : WAD wad: WAD
A WAD object of a WAD containing the title's data. A WAD object of a WAD containing the title's data.
tmd : TMD cert_chain: CertificateChain
The chain of certificates used to verify the contents of a title.
tmd: TMD
A TMD object of the title's TMD. A TMD object of the title's TMD.
ticket : Ticket ticket: Ticket
A Ticket object of the title's Ticket. A Ticket object of the title's Ticket.
content: ContentRegion content: ContentRegion
A ContentRegion object containing the title's contents. A ContentRegion object containing the title's contents.
""" """
def __init__(self): def __init__(self):
self.wad: WAD = WAD() self.wad: _WAD = _WAD()
self.tmd: TMD = TMD() self.cert_chain: _CertificateChain = _CertificateChain()
self.ticket: Ticket = Ticket() self.tmd: _TMD = _TMD()
self.content: ContentRegion = ContentRegion() self.ticket: _Ticket = _Ticket()
self.content: _ContentRegion = _ContentRegion()
def load_wad(self, wad: bytes) -> None: def load_wad(self, wad: bytes) -> None:
""" """
@@ -45,16 +51,19 @@ class Title:
The data for the WAD you wish to load. The data for the WAD you wish to load.
""" """
# Create a new WAD object based on the WAD data provided. # Create a new WAD object based on the WAD data provided.
self.wad = WAD() self.wad = _WAD()
self.wad.load(wad) self.wad.load(wad)
# Load the certificate chain.
self.cert_chain = _CertificateChain()
self.cert_chain.load(self.wad.get_cert_data())
# Load the TMD. # Load the TMD.
self.tmd = TMD() self.tmd = _TMD()
self.tmd.load(self.wad.get_tmd_data()) self.tmd.load(self.wad.get_tmd_data())
# Load the ticket. # Load the ticket.
self.ticket = Ticket() self.ticket = _Ticket()
self.ticket.load(self.wad.get_ticket_data()) self.ticket.load(self.wad.get_ticket_data())
# Load the content. # Load the content.
self.content = ContentRegion() self.content = _ContentRegion()
self.content.load(self.wad.get_content_data(), self.tmd.content_records) self.content.load(self.wad.get_content_data(), self.tmd.content_records)
# Ensure that the Title IDs of the TMD and Ticket match before doing anything else. If they don't, throw an # Ensure that the Title IDs of the TMD and Ticket match before doing anything else. If they don't, throw an
# error because clearly something strange has gone on with the WAD and editing it probably won't work. # error because clearly something strange has gone on with the WAD and editing it probably won't work.
@@ -75,6 +84,8 @@ class Title:
# Set WAD type to ib if the title being packed is boot2. # Set WAD type to ib if the title being packed is boot2.
if self.tmd.title_id == "0000000100000001": if self.tmd.title_id == "0000000100000001":
self.wad.wad_type = "ib" self.wad.wad_type = "ib"
# Dump the certificate chain and set it in the WAD.
self.wad.set_cert_data(self.cert_chain.dump())
# Dump the TMD and set it in the WAD. # Dump the TMD and set it in the WAD.
# This requires updating the content records and number of contents in the TMD first. # This requires updating the content records and number of contents in the TMD first.
self.tmd.content_records = self.content.content_records # This may not be needed because it's a ref already self.tmd.content_records = self.content.content_records # This may not be needed because it's a ref already
@@ -87,6 +98,19 @@ class Title:
self.wad.set_content_data(content_data, content_size) self.wad.set_content_data(content_data, content_size)
return self.wad.dump() return self.wad.dump()
def load_cert_chain(self, cert_chain: bytes) -> None:
"""
Load an existing certificate chain into the title. Note that this will overwrite any existing certificate chain
data for this title.
Parameters
----------
cert_chain: bytes
The data for the certificate chain to load.
"""
self.cert_chain.load(cert_chain)
def load_tmd(self, tmd: bytes) -> None: def load_tmd(self, tmd: bytes) -> None:
""" """
Load existing TMD data into the title. Note that this will overwrite any existing TMD data for this title. Load existing TMD data into the title. Note that this will overwrite any existing TMD data for this title.
@@ -94,9 +118,8 @@ class Title:
Parameters Parameters
---------- ----------
tmd : bytes tmd : bytes
The data for the WAD you wish to load. The data for the TMD to load.
""" """
# Load TMD.
self.tmd.load(tmd) self.tmd.load(tmd)
def load_ticket(self, ticket: bytes) -> None: def load_ticket(self, ticket: bytes) -> None:
@@ -107,9 +130,8 @@ class Title:
Parameters Parameters
---------- ----------
ticket : bytes ticket : bytes
The data for the WAD you wish to load. The data for the Ticket to load.
""" """
# Load Ticket.
self.ticket.load(ticket) self.ticket.load(ticket)
def load_content_records(self) -> None: def load_content_records(self) -> None:
@@ -398,3 +420,34 @@ class Title:
return True return True
else: else:
return False return False
def get_is_signed(self) -> bool:
"""
Uses the certificate chain to verify whether the Title object contains a properly signed title or not. This
verifies both the TMD and Ticket, and if either one fails verification then the title is not considered valid.
This will validate the entire certificate chain. If any part of the chain doesn't match the other pieces, then
this method will raise an exception.
Returns
-------
bool
Whether the title is properly signed or not.
See Also
--------
libWiiPy.title.cert
"""
# The entire chain needs to be verified, so start with the CA cert and work our way down. If anything fails
# along the way, future steps don't matter so exit the descending if's and return False.
try:
if _verify_ca_cert(self.cert_chain.ca_cert) is True:
if _verify_cert_sig(self.cert_chain.ca_cert, self.cert_chain.tmd_cert) is True:
if _verify_tmd_sig(self.cert_chain.tmd_cert, self.tmd) is True:
if _verify_cert_sig(self.cert_chain.ca_cert, self.cert_chain.ticket_cert) is True:
if _verify_ticket_sig(self.cert_chain.ticket_cert, self.ticket) is True:
return True
except ValueError:
raise ValueError("This title's certificate chain is not valid, or does not match the signature type of "
"the TMD/Ticket.")
return False

View File

@@ -83,7 +83,7 @@ class TMD:
self.signature = tmd_data.read(256) self.signature = tmd_data.read(256)
# Signing certificate issuer. # Signing certificate issuer.
tmd_data.seek(0x140) tmd_data.seek(0x140)
self.signature_issuer = str(tmd_data.read(64).decode()) self.signature_issuer = str(tmd_data.read(64).replace(b'\x00', b'').decode())
# TMD version, seems to usually be 0, but I've seen references to other numbers. # TMD version, seems to usually be 0, but I've seen references to other numbers.
tmd_data.seek(0x180) tmd_data.seek(0x180)
self.tmd_version = int.from_bytes(tmd_data.read(1)) self.tmd_version = int.from_bytes(tmd_data.read(1))
@@ -175,7 +175,10 @@ class TMD:
# Padding to 64 bytes. # Padding to 64 bytes.
tmd_data += b'\x00' * 60 tmd_data += b'\x00' * 60
# Signing certificate issuer. # Signing certificate issuer.
tmd_data += str.encode(self.signature_issuer) signature_issuer = self.signature_issuer.encode()
while len(signature_issuer) < 0x40:
signature_issuer += b'\x00'
tmd_data += signature_issuer
# TMD version. # TMD version.
tmd_data += int.to_bytes(self.tmd_version, 1) tmd_data += int.to_bytes(self.tmd_version, 1)
# Certificate Authority CRL version. # Certificate Authority CRL version.

View File

@@ -0,0 +1,62 @@
# "title/wiiload.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy
#
# This code is adapted from "wiiload.py", which can be found on the WiiBrew page for Wiiload.
# https://pastebin.com/4nWAkBpw
#
# See https://wiibrew.org/wiki/Wiiload for details about how Wiiload works
import sys
import zlib
import socket
import struct
def send_bin_wiiload(target_ip: str, bin_data: bytes, name: str) -> None:
"""
Sends an ELF or DOL binary to The Homebrew Channel via Wiiload. This requires the IP address of the console you
want to send the binary to.
Parameters
----------
target_ip: str
The IP address of the console to send the binary to.
bin_data: bytes
The data of the ELF or DOL to send.
name: str
The name of the application being sent.
"""
wii_ip = (target_ip, 4299)
WIILOAD_VERSION_MAJOR=0
WIILOAD_VERSION_MINOR=5
len_uncompressed = len(bin_data)
c_data = zlib.compress(bin_data, 6)
chunk_size = 1024*128
chunks = [c_data[i:i+chunk_size] for i in range(0, len(c_data), chunk_size)]
args = [name]
args = "\x00".join(args) + "\x00"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(wii_ip)
s.send("HAXX")
s.send(struct.pack("B", WIILOAD_VERSION_MAJOR)) # one byte, unsigned
s.send(struct.pack("B", WIILOAD_VERSION_MINOR)) # one byte, unsigned
s.send(struct.pack(">H",len(args))) # bigendian, 2 bytes, unsigned
s.send(struct.pack(">L",len(c_data))) # bigendian, 4 bytes, unsigned
s.send(struct.pack(">L",len_uncompressed)) # bigendian, 4 bytes, unsigned
print(len(chunks),"chunks to send")
for piece in chunks:
s.send(piece)
sys.stdout.write("."); sys.stdout.flush()
sys.stdout.write("\n")
s.send(args)
s.close()
print("done")