diff --git a/src/libWiiPy/title/cert.py b/src/libWiiPy/title/cert.py index 486e33e..63bbe1c 100644 --- a/src/libWiiPy/title/cert.py +++ b/src/libWiiPy/title/cert.py @@ -210,23 +210,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=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\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=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\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 +274,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 +305,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 +334,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: diff --git a/src/libWiiPy/title/ticket.py b/src/libWiiPy/title/ticket.py index 8485ab3..fb85c6a 100644 --- a/src/libWiiPy/title/ticket.py +++ b/src/libWiiPy/title/ticket.py @@ -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. diff --git a/src/libWiiPy/title/tmd.py b/src/libWiiPy/title/tmd.py index 96958d6..bdc2e1a 100644 --- a/src/libWiiPy/title/tmd.py +++ b/src/libWiiPy/title/tmd.py @@ -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.