Added new cert module to parse certs and functions to use them for verification

This commit is contained in:
Campbell 2024-12-15 22:08:51 -05:00
parent f98a3703a4
commit 2fdd808137
Signed by: NinjaCheetah
GPG Key ID: 670C282B3291D63D
3 changed files with 241 additions and 1 deletions

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 *

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

@ -0,0 +1,239 @@
# "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 _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
----------
something : bool
I'm a placeholder attribute for later.
"""
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
def verify_ca_cert(ca_cert: Certificate) -> bool:
"""
Verify a Wii CA certificate using the root public key.
Parameters
----------
ca_cert: Certificate
The CA certificate to verify.
Returns
-------
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')
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.
"""
cert_hash = SHA1.new(target_cert.dump()[576:])
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.
"""
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.
"""
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