Compare commits

...

4 Commits

5 changed files with 122 additions and 46 deletions

View File

@ -43,8 +43,22 @@ class Certificate:
Attributes
----------
something : bool
I'm a placeholder attribute for later.
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
@ -198,7 +212,8 @@ class CertificateChain:
def verify_ca_cert(ca_cert: Certificate) -> bool:
"""
Verify a Wii CA certificate using the root public key.
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
----------
@ -210,23 +225,44 @@ def verify_ca_cert(ca_cert: Certificate) -> bool:
bool
Whether the certificate is valid or not.
"""
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'
b'\xd3\xd0~\xfc\x85 i\xb5\xde\x9b\xb9Q\xa8\xbc\x90\xa2D\x92m7\x92\x95\xae\x946\xaa\xa6\xa3\x02Q'
b'\x0c{\x1d\xed\xd5\xfb \x86\x9d\x7f0\x16\xf6\xbee\xd3\x83\xa1m\xb32\x1b\x955\x18\x90\xb1p\x02'
b'\x93~\xe1\x93\xf5~\x99\xa2GN\x9d8$\xc7\xae\xe3\x85A\xf5g\xe7Q\x8cz\x0e8\xe7\xeb\xafA\x19\x1b'
b'\xcf\xf1{B\xa6\xb4\xed\xe6\xce\x8d\xe71\x8f\x7fR\x04\xb3\x99\x0e"gE\xaf\xd4\x85\xb2D\x93\x00'
b'\x8b\x08\xc7\xf6\xb7\xe5k\x02\xb3\xe8\xfe\x0c\x9d\x85\x9c\xb8\xb6\x82#\xb8\xab\'\xee_e8\x07'
b'\x8b-\xb9\x1e*\x15>\x85\x81\x80r\xa2;m\xd92\x81\x05Oo\xb0\xf6\xf5\xad(>\xca\x0bz\xf3TU\xe0='
b'\xa7\xb6\x83&\xf3\xec\x83J\xf3\x14\x04\x8a\xc6\xdf \xd2\x85\x08g<\xabb\xa2\xc7\xbc\x13\x1aS>'
b'\x0bf\x80k\x1c0fK7#1\xbd\xc4\xb0\xca\xd8\xd1\x1e\xe7\xbb\xd9(UH\xaa\xec\x1ff\xe8!\xb3\xc8'
b'\xa0Gi\x00\xc5\xe6\x88\xe8\x0c\xce<a\xd6\x9c\xbb\xa17\xc6`Ozr\xdd\x8c{>=Q)\r\xaajY{\x08\x1f'
b'\x9d63\xa3Fz5a\t\xac\xa7\xdd}./\xb2\xc1\xae\xb8\xe2\x0fH\x92\xd8\xb9\xf8\xb4oN<\x11\xf4\xf4}'
b'\x8bu}\xfe\xfe\xa3\x89\x9c3Y\\^\xfd\xeb\xcb\xab\xe8A>:\x9a\x80<i5n\xb2\xb2\xad\\\xc4\xc8XE^'
b'\xf5\xf7\xb3\x06D\xb4|d\x06\x8c\xdf\x80\x9fv\x02Z-\xb4F\xe0=|\xf6/4\xe7\x02E{\x02\xa4\xcf]'
b'\x9d\xd5<\xa5:|\xa6)x\x8cg\xca\x08\xbf\xec\xcaC\xa9W\xad\x16\xc9N\x1c\xd8u\xca\x10}\xce~\x01'
b'\x18\xf0\xdfk\xfe\xe5\x1d\xdb\xd9\x91\xc2n`\xcdHX\xaaY,\x82\x00u\xf2\x9fRl\x91|o\xe5@>\xa7'
b'\xd4\xa5\x0c\xec;s\x84\xde\x88n\x82\xd2\xebMNB\xb5\xf2\xb1I\xa8\x1e\xa7\xceqD\xdc)\x94\xcf'
b'\xc4N\x1f\x91\xcb\xd4\x95')
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))
@ -253,6 +289,12 @@ def verify_cert_sig(ca_cert: Certificate, target_cert: Certificate) -> bool:
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:
@ -278,6 +320,10 @@ def verify_tmd_sig(tmd_cert: Certificate, tmd: TMD) -> bool:
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:
@ -303,6 +349,10 @@ def verify_ticket_sig(ticket_cert: Certificate, ticket: Ticket) -> bool:
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:

View File

@ -40,10 +40,10 @@ def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool
"""
# First, create the new 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_ticket(download_ticket(title_id, wiiu_endpoint, endpoint_override))
title.wad.set_cert_data(download_cert(wiiu_endpoint, endpoint_override))
# Download all contents
title.load_content_records()
title.content.content_list = download_contents(title_id, title.tmd, wiiu_endpoint, endpoint_override)
@ -146,9 +146,9 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_overrid
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
----------
@ -163,7 +163,7 @@ def download_cert(wiiu_endpoint: bool = False, endpoint_override: str = None) ->
bytes
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:
endpoint_url = _validate_endpoint(endpoint_override)
else:
@ -175,18 +175,18 @@ def download_cert(wiiu_endpoint: bool = False, endpoint_override: str = None) ->
cetk_url = endpoint_url + "0000000100000002/cetk"
tmd = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content
cetk = requests.get(url=cetk_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content
# Assemble the certificate.
cert = b''
# Assemble the certificate chain.
cert_chain = b''
# Certificate Authority data.
cert += cetk[0x2A4 + 768:]
# Certificate Policy data.
cert += tmd[0x328:0x328 + 768]
# XS data.
cert += cetk[0x2A4:0x2A4 + 768]
# Since the cert is always the same, check the hash to make sure nothing went wildly wrong.
if hashlib.sha1(cert).hexdigest() != "ace0f15d2a851c383fe4657afc3840d6ffe30ad0":
cert_chain += cetk[0x2A4 + 768:]
# Certificate Policy (TMD certificate) data.
cert_chain += tmd[0x328:0x328 + 768]
# XS (Ticket certificate) data.
cert_chain += cetk[0x2A4:0x2A4 + 768]
# Since the cert chain is always the same, check the hash to make sure nothing went wildly wrong.
if hashlib.sha1(cert_chain).hexdigest() != "ace0f15d2a851c383fe4657afc3840d6ffe30ad0":
raise Exception("An unknown error has occurred downloading and creating the certificate.")
return cert
return cert_chain
def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False,

View File

@ -107,7 +107,7 @@ class Ticket:
self.signature = ticket_data.read(256)
# Signature issuer.
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.
ticket_data.seek(0x180)
self.ecdh_data = ticket_data.read(60)
@ -182,7 +182,10 @@ class Ticket:
# Padding to 64 bytes.
ticket_data += b'\x00' * 60
# 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.
ticket_data += self.ecdh_data
# Ticket version.

View File

@ -4,6 +4,7 @@
# See https://wiibrew.org/wiki/Title for details about how titles are formatted
import math
from .cert import CertificateChain
from .content import ContentRegion
from .ticket import Ticket
from .tmd import TMD
@ -19,17 +20,20 @@ class Title:
Attributes
----------
wad : WAD
wad: WAD
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.
ticket : Ticket
ticket: Ticket
A Ticket object of the title's Ticket.
content: ContentRegion
A ContentRegion object containing the title's contents.
"""
def __init__(self):
self.wad: WAD = WAD()
self.cert_chain: CertificateChain = CertificateChain()
self.tmd: TMD = TMD()
self.ticket: Ticket = Ticket()
self.content: ContentRegion = ContentRegion()
@ -47,6 +51,9 @@ class Title:
# Create a new WAD object based on the WAD data provided.
self.wad = 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.
self.tmd = TMD()
self.tmd.load(self.wad.get_tmd_data())
@ -75,6 +82,8 @@ class Title:
# Set WAD type to ib if the title being packed is boot2.
if self.tmd.title_id == "0000000100000001":
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.
# 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
@ -87,6 +96,19 @@ class Title:
self.wad.set_content_data(content_data, content_size)
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:
"""
Load existing TMD data into the title. Note that this will overwrite any existing TMD data for this title.
@ -94,9 +116,8 @@ class Title:
Parameters
----------
tmd : bytes
The data for the WAD you wish to load.
The data for the TMD to load.
"""
# Load TMD.
self.tmd.load(tmd)
def load_ticket(self, ticket: bytes) -> None:
@ -107,9 +128,8 @@ class Title:
Parameters
----------
ticket : bytes
The data for the WAD you wish to load.
The data for the Ticket to load.
"""
# Load Ticket.
self.ticket.load(ticket)
def load_content_records(self) -> None:

View File

@ -83,7 +83,7 @@ class TMD:
self.signature = tmd_data.read(256)
# Signing certificate issuer.
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_data.seek(0x180)
self.tmd_version = int.from_bytes(tmd_data.read(1))
@ -175,7 +175,10 @@ class TMD:
# Padding to 64 bytes.
tmd_data += b'\x00' * 60
# 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_data += int.to_bytes(self.tmd_version, 1)
# Certificate Authority CRL version.