From c1b76fb2d939b7e58e7376518a5186075c485afe Mon Sep 17 00:00:00 2001 From: Pablo Curiel Date: Sun, 15 Oct 2023 17:53:46 +0200 Subject: [PATCH] Rework signature/cert/tik interfaces. * signature: add comments to SignatureType enum entries about the exact signing algorithms and padding schemes used. * signature: rename signatureGetSigType() -> signatureGetTypeFromSignedBlob(). * signature: rename signatureIsValidSigType() -> signatureIsValidType(). * signature: rename signatureGetSigSize() -> signatureGetSigSizeByType(). * signature: rename signatureGetBlockSize() -> signatureGetBlockSizeByType(). * signature: rename signatureGetSig() -> signatureGetSigFromSignedBlob(). * signature: rename signatureGetPayload() -> signatureGetPayloadFromSignedBlob(). * signature: add signatureGetBlockSizeFromSignedBlob(). * cert: add more comments to the code. * cert: update code to match signature interface changes. * cert: add CERT_RSA_PUB_EXP_SIZE macro. * cert: change public_exponent field in CertPublicKeyBlockRsa* structs from u32 to u8 array. * cert: add size field to CertificateChain struct. * cert: rename certGetCommonBlock() -> certGetCommonBlockFromSignedCertBlob. * cert: rename certGetPublicKeySize() -> certGetPublicKeySizeByType(). * cert: rename certGetPublicKeyBlockSize() -> certGetPublicKeyBlockSizeByType(). * cert: rename certIsValidCertificate() -> certIsValidSignedCertBlob(). * cert: rename certGetSignedCertificateSize() -> certGetSignedCertBlobSize(). * cert: rename certGetSignedCertificateHashAreaSize() -> certGetSignedCertBlobHashAreaSize(). * cert: remove certGetPublicKey(), certGetPublicExponent() and certCalculateRawCertificateChainSize(). * cert: add certGetPublicKeyTypeFromCommonBlock(), certGetPublicKeyTypeFromSignedCertBlob(), certGetPublicKeySizeFromSignedCertBlob(), certGetPublicKeyBlockSizeFromSignedCertBlob(), certGetPublicKeyFromSignedCertBlob(), certGetPublicExponentFromSignedCertBlob(), certIsValidCertificate() (w/diff func sig), certGetCommonBlockFromCertificate(), certGetPublicKeyTypeFromCertificate(), certGetPublicKeySizeFromCertificate(), certGetPublicKeyBlockSizeFromCertificate(), certGetPublicKeyFromCertificate(), certGetPublicExponentFromCertificate() and certGetHashAreaSizeFromCertificate() functions. * cert: avoid byteswapping the public key type value in multiple places -- it is now only being done in certGetPublicKeyTypeFromCommonBlock(). * cert: call certFreeCertificateChain() in _certRetrieveCertificateChainBySignatureIssuer() before attempting to retrieve the certificate chain. * cert: other minor changes and corrections. * tik: update code to match signature interface changes. * tik: add missing comments to TikPropertyMask enum entries. * tik: add key_generation, enc_titlekey_str and dec_titlekey_str fields to Ticket struct. * tik: update tikRetrieveTicketByRightsId() to also take in a key_generation argument, instead of getting it from the rights ID (which could fail if it's using a key generation lower than HOS 3.0.1) or the key_generation field from the common ticket block (which could fail if the ticket has been tampered by certain tools). * tik: rename tikGetCommonBlock() -> tikGetCommonBlockFromSignedTicketBlob(). * tik: change function signature for tikGetTicketSectionRecordsBlockSize(). * tik: rename tikIsValidTicket() -> tikIsValidSignedTicketBlob(). * tik: rename tikGetSignedTicketSize() -> tikGetSignedTicketBlobSize(). * tik: rename tikGetSignedTicketHashAreaSize() -> tikGetSignedTicketBlobHashAreaSize(). * tik: rename tikGetEncryptedTitleKeyFromTicket() -> tikGetEncryptedTitleKey(). * tik: add tikIsValidTicket() (w/diff func sig), tikGetCommonBlockFromTicket(), tikGetHashAreaSizeFromTicket(), tikFixTamperedCommonTicket(), tikVerifyRsa2048Sha256Signature() and tikDecryptVolatileTicket() functions. Ticket signature verification is only carried out for common tickets in tikFixTamperedCommonTicket(). * tik: change argument order in tikGetTicketEntryOffsetFromTicketList() and tikRetrieveTicketEntryFromTicketBin(). * tik: add TIK_COMMON_CERT_NAME and TIK_DEV_CERT_ISSUER macros. * tik: use a scoped lock when calling tikRetrieveTicketFromEsSaveDataByRightsId(). * tik: simplify certificate chain retrieval steps in tikConvertPersonalizedTicketToCommonTicket() by always using the XS00000020 certificate. * tik: wipe license_type and property_mask fields in tikConvertPersonalizedTicketToCommonTicket(). * tik: other minor changes and corrections. Other changes include: * keys: fix key generation checks in keysGetNcaKeyAreaKeyEncryptionKey() and keysGetTicketCommonKey(). * rsa: move core logic from rsa2048VerifySha256BasedPssSignature() into a new function: rsa2048VerifySha256BasedSignature(). * rsa: add rsa2048VerifySha256BasedPkcs1v15Signature() function. --- code_templates/nxdt_rw_poc.c | 20 +- include/core/cert.h | 204 ++++++++++----- include/core/nca.h | 4 +- include/core/npdm.h | 2 +- include/core/rsa.h | 8 +- include/core/signature.h | 53 ++-- include/core/tik.h | 68 +++-- source/core/cert.c | 112 ++++---- source/core/keys.c | 4 +- source/core/nca.c | 5 +- source/core/rsa.c | 93 ++++--- source/core/tik.c | 477 +++++++++++++++++++++++------------ 12 files changed, 663 insertions(+), 387 deletions(-) diff --git a/code_templates/nxdt_rw_poc.c b/code_templates/nxdt_rw_poc.c index 7599ca3..da6d90e 100644 --- a/code_templates/nxdt_rw_poc.c +++ b/code_templates/nxdt_rw_poc.c @@ -2869,8 +2869,6 @@ static bool saveTicket(void *userdata) NcaContext *nca_ctx = NULL; Ticket tik = {0}; - TikCommonBlock *tik_common_block = NULL; - char enc_titlekey_str[33] = {0}; u32 crc = 0; char *filename = NULL; @@ -2916,21 +2914,14 @@ static bool saveTicket(void *userdata) goto end; } - if (!tik.size) + if (!nca_ctx->titlekey_retrieved) { consolePrint("failed to retrieve ticket (unavailable?)\ntry launching nxdumptool while overriding the title you wish to dump a ticket from\n"); goto end; } - /* Retrieve ticket common block. */ - if (!(tik_common_block = tikGetCommonBlock(tik.data))) - { - consolePrint("failed to get tik common block\n"); - goto end; - } - /* Remove console-specific data, if needed. */ - if (remove_console_data && tik_common_block->titlekey_type == TikTitleKeyType_Personalized && !tikConvertPersonalizedTicketToCommonTicket(&tik, NULL, NULL)) + if (remove_console_data && tikIsPersonalizedTicket(&tik) && !tikConvertPersonalizedTicketToCommonTicket(&tik, NULL, NULL)) { consolePrint("failed to convert personalized ticket to common ticket\n"); goto end; @@ -2945,10 +2936,9 @@ static bool saveTicket(void *userdata) if (!saveFileData(filename, tik.data, tik.size)) goto end; - utilsGenerateHexStringFromData(enc_titlekey_str, MAX_ELEMENTS(enc_titlekey_str), tik.enc_titlekey, sizeof(tik.enc_titlekey), false); - consolePrint("rights id: %s\n", tik.rights_id_str); - consolePrint("titlekey: %s\n\n", enc_titlekey_str); + consolePrint("encrypted titlekey: %s\n", tik.enc_titlekey_str); + consolePrint("decrypted titlekey: %s\n\n", tik.dec_titlekey_str); consolePrint("successfully saved ticket as \"%s\"\n", filename); success = true; @@ -5003,7 +4993,7 @@ static void nspThreadFunc(void *arg) bool retrieve_tik_cert = (!remove_titlekey_crypto && tik.size > 0); if (retrieve_tik_cert) { - if (!(tik_common_block = tikGetCommonBlock(tik.data))) + if (!(tik_common_block = tikGetCommonBlockFromTicket(&tik))) { consolePrint("tik common block failed"); goto end; diff --git a/include/core/cert.h b/include/core/cert.h index 5f1698f..a778524 100644 --- a/include/core/cert.h +++ b/include/core/cert.h @@ -30,17 +30,17 @@ extern "C" { #endif -#define SIGNED_CERT_MAX_SIZE sizeof(CertSigRsa4096PubKeyRsa4096) #define SIGNED_CERT_MIN_SIZE sizeof(CertSigHmac160PubKeyEcc480) +#define SIGNED_CERT_MAX_SIZE sizeof(CertSigRsa4096PubKeyRsa4096) + +#define CERT_RSA_PUB_EXP_SIZE 4 #define GENERATE_CERT_STRUCT(sigtype, pubkeytype, certsize) \ -\ typedef struct { \ SignatureBlock##sigtype sig_block; \ CertCommonBlock cert_common_block; \ CertPublicKeyBlock##pubkeytype pub_key_block; \ } CertSig##sigtype##PubKey##pubkeytype; \ -\ NXDT_ASSERT(CertSig##sigtype##PubKey##pubkeytype, certsize); typedef enum { @@ -63,7 +63,7 @@ NXDT_ASSERT(CertCommonBlock, 0x88); /// RSA-4096 public key block. Placed after the certificate common block. typedef struct { u8 public_key[0x200]; - u32 public_exponent; + u8 public_exponent[CERT_RSA_PUB_EXP_SIZE]; u8 padding[0x34]; } CertPublicKeyBlockRsa4096; @@ -72,7 +72,7 @@ NXDT_ASSERT(CertPublicKeyBlockRsa4096, 0x238); /// RSA-2048 public key block. Placed after the certificate common block. typedef struct { u8 public_key[0x100]; - u32 public_exponent; + u8 public_exponent[CERT_RSA_PUB_EXP_SIZE]; u8 padding[0x34]; } CertPublicKeyBlockRsa2048; @@ -133,10 +133,11 @@ typedef struct { u8 data[SIGNED_CERT_MAX_SIZE]; ///< Raw certificate data. } Certificate; -/// Used to store two or more certificates. +/// Used to store multiple certificates. typedef struct { - u32 count; - Certificate *certs; + u32 count; ///< Number of certificates in this chain. + u64 size; ///< Raw certificate chain size (when concatenated). + Certificate *certs; ///< Certificate array. } CertificateChain; /// Retrieves a certificate by its name (e.g. "CA00000003", "XS00000020", etc.). @@ -145,16 +146,136 @@ bool certRetrieveCertificateByName(Certificate *dst, const char *name); /// Retrieves a certificate chain by a full signature issuer string (e.g. "Root-CA00000003-XS00000020"). bool certRetrieveCertificateChainBySignatureIssuer(CertificateChain *dst, const char *issuer); -/// Returns a pointer to a dynamically allocated buffer that contains the raw contents from the certificate chain matching the input signature issuer. It must be freed by the user. -/// NULL is returned if an error occurs. +/// Returns a pointer to a dynamically allocated buffer that holds the concatenated raw contents from the certificate chain matching the input signature issuer. +/// The returned buffer must be freed by the user. +/// Returns NULL if an error occurs. u8 *certGenerateRawCertificateChainBySignatureIssuer(const char *issuer, u64 *out_size); -/// Returns a pointer to a dynamically allocated buffer that contains the raw contents from the certificate chain matching the input Rights ID (stored in the inserted gamecard). -/// It must be freed by the user. -/// NULL is returned if an error occurs. +/// Returns a pointer to a dynamically allocated buffer that holds the concatenated raw contents from the certificate chain matching the input rights ID in the inserted gamecard. +/// The returned buffer must be freed by the user. +/// Returns NULL if an error occurs. u8 *certRetrieveRawCertificateChainFromGameCardByRightsId(const FsRightsId *id, u64 *out_size); -/// Helper inline functions. +/// General purpose helper inline functions. + +NX_INLINE bool certIsValidPublicKeyType(u32 type) +{ + return (type < CertPubKeyType_Count); +} + +NX_INLINE u64 certGetPublicKeySizeByType(u32 type) +{ + return (u64)(type == CertPubKeyType_Rsa4096 ? MEMBER_SIZE(CertPublicKeyBlockRsa4096, public_key) : \ + (type == CertPubKeyType_Rsa2048 ? MEMBER_SIZE(CertPublicKeyBlockRsa2048, public_key) : \ + (type == CertPubKeyType_Ecc480 ? MEMBER_SIZE(CertPublicKeyBlockEcc480, public_key) : 0))); +} + +NX_INLINE u64 certGetPublicKeyBlockSizeByType(u32 type) +{ + return (u64)(type == CertPubKeyType_Rsa4096 ? sizeof(CertPublicKeyBlockRsa4096) : \ + (type == CertPubKeyType_Rsa2048 ? sizeof(CertPublicKeyBlockRsa2048) : \ + (type == CertPubKeyType_Ecc480 ? sizeof(CertPublicKeyBlockEcc480) : 0))); +} + +NX_INLINE u32 certGetPublicKeyTypeFromCommonBlock(CertCommonBlock *cert_common_block) +{ + return (cert_common_block ? __builtin_bswap32(cert_common_block->pub_key_type) : CertPubKeyType_Count); +} + +/// Helper inline functions for signed certificate blobs. + +NX_INLINE CertCommonBlock *certGetCommonBlockFromSignedCertBlob(void *buf) +{ + return (CertCommonBlock*)signatureGetPayloadFromSignedBlob(buf, true); +} + +NX_INLINE bool certIsValidSignedCertBlob(void *buf) +{ + CertCommonBlock *cert_common_block = certGetCommonBlockFromSignedCertBlob(buf); + return (cert_common_block && certIsValidPublicKeyType(certGetPublicKeyTypeFromCommonBlock(cert_common_block))); +} + +NX_INLINE u32 certGetPublicKeyTypeFromSignedCertBlob(void *buf) +{ + return (certIsValidSignedCertBlob(buf) ? certGetPublicKeyTypeFromCommonBlock(certGetCommonBlockFromSignedCertBlob(buf)) : CertPubKeyType_Count); +} + +NX_INLINE u64 certGetPublicKeySizeFromSignedCertBlob(void *buf) +{ + return certGetPublicKeySizeByType(certGetPublicKeyTypeFromSignedCertBlob(buf)); +} + +NX_INLINE u64 certGetPublicKeyBlockSizeFromSignedCertBlob(void *buf) +{ + return certGetPublicKeyBlockSizeByType(certGetPublicKeyTypeFromSignedCertBlob(buf)); +} + +NX_INLINE u8 *certGetPublicKeyFromSignedCertBlob(void *buf) +{ + return (certIsValidSignedCertBlob(buf) ? ((u8*)certGetCommonBlockFromSignedCertBlob(buf) + sizeof(CertCommonBlock)) : NULL); +} + +NX_INLINE u8 *certGetPublicExponentFromSignedCertBlob(void *buf) +{ + u32 pub_key_type = certGetPublicKeyTypeFromSignedCertBlob(buf); + u8 *public_key = certGetPublicKeyFromSignedCertBlob(buf); + return (pub_key_type < CertPubKeyType_Ecc480 ? (public_key + certGetPublicKeySizeByType(pub_key_type)) : NULL); // Only allow RSA public key types. +} + +NX_INLINE u64 certGetSignedCertBlobSize(void *buf) +{ + return (certIsValidSignedCertBlob(buf) ? (signatureGetBlockSizeFromSignedBlob(buf, true) + sizeof(CertCommonBlock) + certGetPublicKeyBlockSizeFromSignedCertBlob(buf)) : 0); +} + +NX_INLINE u64 certGetSignedCertBlobHashAreaSize(void *buf) +{ + return (certIsValidSignedCertBlob(buf) ? (sizeof(CertCommonBlock) + certGetPublicKeyBlockSizeFromSignedCertBlob(buf)) : 0); +} + +/// Helper inline functions for Certificate elements. + +NX_INLINE bool certIsValidCertificate(Certificate *cert) +{ + return (cert && cert->type > CertType_None && cert->type < CertType_Count && cert->size >= SIGNED_CERT_MIN_SIZE && cert->size <= SIGNED_CERT_MAX_SIZE && \ + certIsValidSignedCertBlob(cert->data)); +} + +NX_INLINE CertCommonBlock *certGetCommonBlockFromCertificate(Certificate *cert) +{ + return (certIsValidCertificate(cert) ? certGetCommonBlockFromSignedCertBlob(cert->data) : NULL); +} + +NX_INLINE u32 certGetPublicKeyTypeFromCertificate(Certificate *cert) +{ + return (certIsValidCertificate(cert) ? certGetPublicKeyTypeFromSignedCertBlob(cert->data) : CertPubKeyType_Count); +} + +NX_INLINE u64 certGetPublicKeySizeFromCertificate(Certificate *cert) +{ + return (certIsValidCertificate(cert) ? certGetPublicKeySizeFromSignedCertBlob(cert->data) : 0); +} + +NX_INLINE u64 certGetPublicKeyBlockSizeFromCertificate(Certificate *cert) +{ + return (certIsValidCertificate(cert) ? certGetPublicKeyBlockSizeFromSignedCertBlob(cert->data) : 0); +} + +NX_INLINE u8 *certGetPublicKeyFromCertificate(Certificate *cert) +{ + return (certIsValidCertificate(cert) ? certGetPublicKeyFromSignedCertBlob(cert->data) : NULL); +} + +NX_INLINE u8 *certGetPublicExponentFromCertificate(Certificate *cert) +{ + return (certIsValidCertificate(cert) ? certGetPublicExponentFromSignedCertBlob(cert->data) : NULL); +} + +NX_INLINE u64 certGetHashAreaSizeFromCertificate(Certificate *cert) +{ + return (certIsValidCertificate(cert) ? certGetSignedCertBlobHashAreaSize(cert->data) : 0); +} + +/// Helper inline functions for CertificateChain elements. NX_INLINE void certFreeCertificateChain(CertificateChain *chain) { @@ -163,61 +284,6 @@ NX_INLINE void certFreeCertificateChain(CertificateChain *chain) memset(chain, 0, sizeof(CertificateChain)); } -NX_INLINE CertCommonBlock *certGetCommonBlock(void *buf) -{ - return (CertCommonBlock*)signatureGetPayload(buf, true); -} - -NX_INLINE bool certIsValidPublicKeyType(u32 type) -{ - return (type == CertPubKeyType_Rsa4096 || type == CertPubKeyType_Rsa2048 || type == CertPubKeyType_Ecc480); -} - -NX_INLINE u8 *certGetPublicKey(CertCommonBlock *cert_common_block) -{ - return ((cert_common_block != NULL && certIsValidPublicKeyType(__builtin_bswap32(cert_common_block->pub_key_type))) ? ((u8*)cert_common_block + sizeof(CertCommonBlock)) : NULL); -} - -NX_INLINE u64 certGetPublicKeySize(u32 type) -{ - return (u64)(type == CertPubKeyType_Rsa4096 ? MEMBER_SIZE(CertPublicKeyBlockRsa4096, public_key) : \ - (type == CertPubKeyType_Rsa2048 ? MEMBER_SIZE(CertPublicKeyBlockRsa2048, public_key) : \ - (type == CertPubKeyType_Ecc480 ? MEMBER_SIZE(CertPublicKeyBlockEcc480, public_key) : 0))); -} - -NX_INLINE u64 certGetPublicKeyBlockSize(u32 type) -{ - return (u64)(type == CertPubKeyType_Rsa4096 ? sizeof(CertPublicKeyBlockRsa4096) : \ - (type == CertPubKeyType_Rsa2048 ? sizeof(CertPublicKeyBlockRsa2048) : \ - (type == CertPubKeyType_Ecc480 ? sizeof(CertPublicKeyBlockEcc480) : 0))); -} - -NX_INLINE u32 certGetPublicExponent(CertCommonBlock *cert_common_block) -{ - u8 *public_key = certGetPublicKey(cert_common_block); - return ((cert_common_block != NULL && public_key != NULL) ? __builtin_bswap32(*((u32*)(public_key + certGetPublicKeySize(__builtin_bswap32(cert_common_block->pub_key_type))))) : 0); -} - -NX_INLINE bool certIsValidCertificate(void *buf) -{ - CertCommonBlock *cert_common_block = certGetCommonBlock(buf); - return (cert_common_block != NULL && certIsValidPublicKeyType(__builtin_bswap32(cert_common_block->pub_key_type))); -} - -NX_INLINE u64 certGetSignedCertificateSize(void *buf) -{ - if (!certIsValidCertificate(buf)) return 0; - CertCommonBlock *cert_common_block = certGetCommonBlock(buf); - return (signatureGetBlockSize(signatureGetSigType(buf, true)) + (u64)sizeof(CertCommonBlock) + certGetPublicKeyBlockSize(__builtin_bswap32(cert_common_block->pub_key_type))); -} - -NX_INLINE u64 certGetSignedCertificateHashAreaSize(void *buf) -{ - if (!certIsValidCertificate(buf)) return 0; - CertCommonBlock *cert_common_block = certGetCommonBlock(buf); - return ((u64)sizeof(CertCommonBlock) + certGetPublicKeyBlockSize(__builtin_bswap32(cert_common_block->pub_key_type))); -} - #ifdef __cplusplus } #endif diff --git a/include/core/nca.h b/include/core/nca.h index eb02434..464b437 100644 --- a/include/core/nca.h +++ b/include/core/nca.h @@ -95,7 +95,7 @@ typedef enum { NcaKeyGeneration_Since1400NUP = 14, ///< 14.0.0 - 14.1.2. NcaKeyGeneration_Since1500NUP = 15, ///< 15.0.0 - 15.0.1. NcaKeyGeneration_Since1600NUP = 16, ///< 16.0.0 - 16.1.0. - NcaKeyGeneration_Since1700NUP = 17, ///< 17.0.0. + NcaKeyGeneration_Since1700NUP = 17, ///< 17.0.0+. NcaKeyGeneration_Current = NcaKeyGeneration_Since1700NUP, NcaKeyGeneration_Max = 32 } NcaKeyGeneration; @@ -111,7 +111,7 @@ typedef enum { /// TODO: update on signature keygen changes. typedef enum { NcaSignatureKeyGeneration_Since100NUP = 0, ///< 1.0.0 - 8.1.1. - NcaSignatureKeyGeneration_Since900NUP = 1, ///< 9.0.0 - 17.0.0. + NcaSignatureKeyGeneration_Since900NUP = 1, ///< 9.0.0+. NcaSignatureKeyGeneration_Current = NcaSignatureKeyGeneration_Since900NUP, NcaSignatureKeyGeneration_Max = (NcaSignatureKeyGeneration_Current + 1) } NcaSignatureKeyGeneration; diff --git a/include/core/npdm.h b/include/core/npdm.h index 2ea0bb6..fe691ba 100644 --- a/include/core/npdm.h +++ b/include/core/npdm.h @@ -43,7 +43,7 @@ extern "C" { /// TODO: update on signature keygen changes. typedef enum { NpdmSignatureKeyGeneration_Since100NUP = 0, ///< 1.0.0 - 8.1.1. - NpdmSignatureKeyGeneration_Since900NUP = 1, ///< 9.0.0 - 17.0.0. + NpdmSignatureKeyGeneration_Since900NUP = 1, ///< 9.0.0+. NpdmSignatureKeyGeneration_Current = NpdmSignatureKeyGeneration_Since900NUP, NpdmSignatureKeyGeneration_Max = (NpdmSignatureKeyGeneration_Current + 1) } NpdmSignatureKeyGeneration; diff --git a/include/core/rsa.h b/include/core/rsa.h index 51581dc..3f9a774 100644 --- a/include/core/rsa.h +++ b/include/core/rsa.h @@ -36,11 +36,17 @@ extern "C" { #define RSA2048_PUBKEY_SIZE RSA2048_BYTES /// Verifies a RSA-2048-PSS with SHA-256 signature. +/// Suitable for NCA and NPDM signatures. /// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively. bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size); +/// Verifies a RSA-2048-PKCS#1 v1.5 with SHA-256 signature. +/// Suitable for ticket and certificate chain signatures. +/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively. +bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size); + /// Performs RSA-2048-OAEP decryption. -/// Suitable to decrypt the titlekey block from tickets with personalized crypto. +/// Suitable to decrypt the titlekey block from personalized tickets. /// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively. /// 'label' and 'label_size' arguments are optional -- if not needed, these may be set to NULL and 0, respectively. bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \ diff --git a/include/core/signature.h b/include/core/signature.h index e00cfbe..58a237e 100644 --- a/include/core/signature.h +++ b/include/core/signature.h @@ -29,13 +29,13 @@ extern "C" { #endif typedef enum { - SignatureType_Rsa4096Sha1 = 0x10000, - SignatureType_Rsa2048Sha1 = 0x10001, - SignatureType_Ecc480Sha1 = 0x10002, - SignatureType_Rsa4096Sha256 = 0x10003, - SignatureType_Rsa2048Sha256 = 0x10004, - SignatureType_Ecc480Sha256 = 0x10005, - SignatureType_Hmac160Sha1 = 0x10006 + SignatureType_Rsa4096Sha1 = 0x10000, ///< RSA-4096 PKCS#1 v1.5 + SHA-1. + SignatureType_Rsa2048Sha1 = 0x10001, ///< RSA-2048 PKCS#1 v1.5 + SHA-1. + SignatureType_Ecc480Sha1 = 0x10002, ///< Unpadded ECDSA + SHA-1. + SignatureType_Rsa4096Sha256 = 0x10003, ///< RSA-4096 PKCS#1 v1.5 + SHA-256. + SignatureType_Rsa2048Sha256 = 0x10004, ///< RSA-2048 PKCS#1 v1.5 + SHA-256. + SignatureType_Ecc480Sha256 = 0x10005, ///< Unpadded ECDSA + SHA-256. + SignatureType_Hmac160Sha1 = 0x10006 ///< HMAC-SHA1-160. } SignatureType; typedef struct { @@ -72,25 +72,14 @@ NXDT_ASSERT(SignatureBlockHmac160, 0x40); /// Helper inline functions. -NX_INLINE u32 signatureGetSigType(void *buf, bool byteswap) -{ - if (!buf) return 0; - return (byteswap ? __builtin_bswap32(*((u32*)buf)) : *((u32*)buf)); -} - -NX_INLINE bool signatureIsValidSigType(u32 type) +NX_INLINE bool signatureIsValidType(u32 type) { return (type == SignatureType_Rsa4096Sha1 || type == SignatureType_Rsa2048Sha1 || type == SignatureType_Ecc480Sha1 || \ type == SignatureType_Rsa4096Sha256 || type == SignatureType_Rsa2048Sha256 || type == SignatureType_Ecc480Sha256 || \ type == SignatureType_Hmac160Sha1); } -NX_INLINE u8 *signatureGetSig(void *buf) -{ - return (buf ? ((u8*)buf + 4) : NULL); -} - -NX_INLINE u64 signatureGetSigSize(u32 type) +NX_INLINE u64 signatureGetSigSizeByType(u32 type) { return (u64)((type == SignatureType_Rsa4096Sha1 || type == SignatureType_Rsa4096Sha256) ? MEMBER_SIZE(SignatureBlockRsa4096, signature) : \ ((type == SignatureType_Rsa2048Sha1 || type == SignatureType_Rsa2048Sha256) ? MEMBER_SIZE(SignatureBlockRsa2048, signature) : \ @@ -98,7 +87,7 @@ NX_INLINE u64 signatureGetSigSize(u32 type) (type == SignatureType_Hmac160Sha1 ? MEMBER_SIZE(SignatureBlockHmac160, signature) : 0)))); } -NX_INLINE u64 signatureGetBlockSize(u32 type) +NX_INLINE u64 signatureGetBlockSizeByType(u32 type) { return (u64)((type == SignatureType_Rsa4096Sha1 || type == SignatureType_Rsa4096Sha256) ? sizeof(SignatureBlockRsa4096) : \ ((type == SignatureType_Rsa2048Sha1 || type == SignatureType_Rsa2048Sha256) ? sizeof(SignatureBlockRsa2048) : \ @@ -106,11 +95,27 @@ NX_INLINE u64 signatureGetBlockSize(u32 type) (type == SignatureType_Hmac160Sha1 ? sizeof(SignatureBlockHmac160) : 0)))); } -NX_INLINE void *signatureGetPayload(void *buf, bool big_endian_sig_type) +NX_INLINE u32 signatureGetTypeFromSignedBlob(void *buf, bool big_endian_sig_type) +{ + if (!buf) return 0; + return (big_endian_sig_type ? __builtin_bswap32(*((u32*)buf)) : *((u32*)buf)); +} + +NX_INLINE u8 *signatureGetSigFromSignedBlob(void *buf) +{ + return (buf ? ((u8*)buf + 4) : NULL); +} + +NX_INLINE u64 signatureGetBlockSizeFromSignedBlob(void *buf, bool big_endian_sig_type) +{ + return signatureGetBlockSizeByType(signatureGetTypeFromSignedBlob(buf, big_endian_sig_type)); +} + +NX_INLINE void *signatureGetPayloadFromSignedBlob(void *buf, bool big_endian_sig_type) { if (!buf) return NULL; - u32 sig_type = signatureGetSigType(buf, big_endian_sig_type); - return (signatureIsValidSigType(sig_type) ? (void*)((u8*)buf + signatureGetBlockSize(sig_type)) : NULL); + u32 sig_type = signatureGetTypeFromSignedBlob(buf, big_endian_sig_type); + return (signatureIsValidType(sig_type) ? (void*)((u8*)buf + signatureGetBlockSizeByType(sig_type)) : NULL); } #ifdef __cplusplus diff --git a/include/core/tik.h b/include/core/tik.h index 10c45ed..02d9e6a 100644 --- a/include/core/tik.h +++ b/include/core/tik.h @@ -30,8 +30,8 @@ extern "C" { #endif -#define SIGNED_TIK_MAX_SIZE 0x400 /* Max ticket entry size in the ES ticket system savedata file. */ #define SIGNED_TIK_MIN_SIZE sizeof(TikSigHmac160) /* Assuming no ESV1/ESV2 records are available. */ +#define SIGNED_TIK_MAX_SIZE 0x400 /* Max ticket entry size in the ES ticket system savedata file. */ #define GENERATE_TIK_STRUCT(sigtype, tiksize) \ typedef struct { \ @@ -59,12 +59,12 @@ typedef enum { typedef enum { TikPropertyMask_None = 0, - TikPropertyMask_PreInstallation = BIT(0), - TikPropertyMask_SharedTitle = BIT(1), - TikPropertyMask_AllContents = BIT(2), - TikPropertyMask_DeviceLinkIndepedent = BIT(3), - TikPropertyMask_Volatile = BIT(4), ///< Used to determine if the ticket copy inside ticket.bin should be encrypted or not. - TikPropertyMask_ELicenseRequired = BIT(5), ///< Used to determine if the console should connect to the Internet to perform elicense verification. + TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats. + TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats. + TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats. + TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console. + TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin should be encrypted or not. + TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification. TikPropertyMask_Count = 6 ///< Total values supported by this enum. } TikPropertyMask; @@ -174,14 +174,18 @@ typedef struct { u8 type; ///< TikType. u64 size; ///< Raw ticket size. u8 data[SIGNED_TIK_MAX_SIZE]; ///< Raw ticket data. + u8 key_generation; ///< NcaKeyGeneration. u8 enc_titlekey[0x10]; ///< Titlekey with titlekek crypto (RSA-OAEP unwrapped if dealing with a TikTitleKeyType_Personalized ticket). + char enc_titlekey_str[0x21]; ///< Character string representation of enc_titlekey. u8 dec_titlekey[0x10]; ///< Titlekey without titlekek crypto. Ready to use for NCA FS section decryption. + char dec_titlekey_str[0x21]; ///< Character string representation of dec_titlekey. char rights_id_str[0x21]; ///< Character string representation of the rights ID from the ticket. } Ticket; -/// Retrieves a ticket from either the ES ticket system savedata file (eMMC BIS System partition) or the secure hash FS partition from an inserted gamecard, using a Rights ID value. -/// Titlekey is also RSA-OAEP unwrapped (if needed) and titlekek decrypted right away. -bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, bool use_gamecard); +/// Retrieves a ticket from either the ES ticket system savedata file (eMMC BIS System partition) or the secure Hash FS partition from an inserted gamecard. +/// Both the input rights ID and key generation values must have been retrieved from a NCA that depends on the desired ticket. +/// Titlekey is also RSA-OAEP unwrapped (if needed) and titlekek-decrypted right away. +bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, u8 key_generation, bool use_gamecard); /// Converts a TikTitleKeyType_Personalized ticket into a TikTitleKeyType_Common ticket and optionally generates a raw certificate chain for the new signature issuer. /// Bear in mind the 'size' member from the Ticket parameter will be updated by this function to remove any possible references to ESV1/ESV2 records. @@ -189,15 +193,16 @@ bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, bool use_gam /// certGenerateRawCertificateChainBySignatureIssuer() is used internally, so the output buffer must be freed by the user. bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_chain, u64 *out_raw_cert_chain_size); -/// Helper inline functions. +/// Helper inline functions for signed ticket blobs. -NX_INLINE TikCommonBlock *tikGetCommonBlock(void *buf) +NX_INLINE TikCommonBlock *tikGetCommonBlockFromSignedTicketBlob(void *buf) { - return (TikCommonBlock*)signatureGetPayload(buf, false); + return (TikCommonBlock*)signatureGetPayloadFromSignedBlob(buf, false); } -NX_INLINE u64 tikGetTicketSectionRecordsBlockSize(TikCommonBlock *tik_common_block) +NX_INLINE u64 tikGetSectionRecordsSizeFromSignedTicketBlob(void *buf) { + TikCommonBlock *tik_common_block = tikGetCommonBlockFromSignedTicketBlob(buf); if (!tik_common_block) return 0; u64 offset = sizeof(TikCommonBlock), out_size = 0; @@ -212,26 +217,43 @@ NX_INLINE u64 tikGetTicketSectionRecordsBlockSize(TikCommonBlock *tik_common_blo return out_size; } -NX_INLINE bool tikIsValidTicket(void *buf) +NX_INLINE bool tikIsValidSignedTicketBlob(void *buf) { - u64 ticket_size = (signatureGetBlockSize(signatureGetSigType(buf, false)) + sizeof(TikCommonBlock)); - return (ticket_size > sizeof(TikCommonBlock) && (ticket_size + tikGetTicketSectionRecordsBlockSize(tikGetCommonBlock(buf))) <= SIGNED_TIK_MAX_SIZE); + u64 ticket_size = (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock)); + return (ticket_size > sizeof(TikCommonBlock) && (ticket_size + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) <= SIGNED_TIK_MAX_SIZE); } -NX_INLINE u64 tikGetSignedTicketSize(void *buf) +NX_INLINE u64 tikGetSignedTicketBlobSize(void *buf) { - return (tikIsValidTicket(buf) ? (signatureGetBlockSize(signatureGetSigType(buf, false)) + sizeof(TikCommonBlock) + tikGetTicketSectionRecordsBlockSize(tikGetCommonBlock(buf))) : 0); + return (tikIsValidSignedTicketBlob(buf) ? (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0); } -NX_INLINE u64 tikGetSignedTicketHashAreaSize(void *buf) +NX_INLINE u64 tikGetSignedTicketBlobHashAreaSize(void *buf) { - return (tikIsValidTicket(buf) ? (sizeof(TikCommonBlock) + tikGetTicketSectionRecordsBlockSize(tikGetCommonBlock(buf))) : 0); + return (tikIsValidSignedTicketBlob(buf) ? (sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0); +} + +/// Helper inline functions for Ticket elements. + +NX_INLINE bool tikIsValidTicket(Ticket *tik) +{ + return (tik && tik->type > TikType_None && tik->type < TikType_Count && tik->size >= SIGNED_TIK_MIN_SIZE && tik->size <= SIGNED_TIK_MAX_SIZE && tikIsValidSignedTicketBlob(tik->data)); +} + +NX_INLINE TikCommonBlock *tikGetCommonBlockFromTicket(Ticket *tik) +{ + return (tikIsValidTicket(tik) ? tikGetCommonBlockFromSignedTicketBlob(tik->data) : NULL); } NX_INLINE bool tikIsPersonalizedTicket(Ticket *tik) { - TikCommonBlock *tik_common_block = tikGetCommonBlock(tik->data); - return (tik_common_block != NULL && tik_common_block->titlekey_type == TikTitleKeyType_Personalized); + TikCommonBlock *tik_common_block = tikGetCommonBlockFromTicket(tik); + return (tik_common_block ? (tik_common_block->titlekey_type == TikTitleKeyType_Personalized) : false); +} + +NX_INLINE u64 tikGetHashAreaSizeFromTicket(Ticket *tik) +{ + return (tikIsValidTicket(tik) ? tikGetSignedTicketBlobHashAreaSize(tik->data) : 0); } #ifdef __cplusplus diff --git a/source/core/cert.c b/source/core/cert.c index 3869177..4bf5cd4 100644 --- a/source/core/cert.c +++ b/source/core/cert.c @@ -46,7 +46,6 @@ static u8 certGetCertificateType(void *data, u64 data_size); static bool _certRetrieveCertificateChainBySignatureIssuer(CertificateChain *dst, const char *issuer); static u32 certGetCertificateCountInSignatureIssuer(const char *issuer); -static u64 certCalculateRawCertificateChainSize(const CertificateChain *chain); static void certCopyCertificateChainDataToMemoryBuffer(void *dst, const CertificateChain *chain); bool certRetrieveCertificateByName(Certificate *dst, const char *name) @@ -99,25 +98,27 @@ u8 *certGenerateRawCertificateChainBySignatureIssuer(const char *issuer, u64 *ou CertificateChain chain = {0}; u8 *raw_chain = NULL; - u64 raw_chain_size = 0; + /* Get full certificate chain using the provided issuer. */ if (!certRetrieveCertificateChainBySignatureIssuer(&chain, issuer)) { LOG_MSG_ERROR("Error retrieving certificate chain for \"%s\"!", issuer); - return NULL; - } - - raw_chain_size = certCalculateRawCertificateChainSize(&chain); - - raw_chain = malloc(raw_chain_size); - if (!raw_chain) - { - LOG_MSG_ERROR("Unable to allocate memory for raw \"%s\" certificate chain! (0x%lX).", issuer, raw_chain_size); goto end; } + /* Allocate memory for the raw certificate chain. */ + raw_chain = malloc(chain.size); + if (!raw_chain) + { + LOG_MSG_ERROR("Unable to allocate memory for raw \"%s\" certificate chain! (0x%lX).", issuer, chain.size); + goto end; + } + + /* Copy all certificates to the allocated buffer. */ certCopyCertificateChainDataToMemoryBuffer(raw_chain, &chain); - *out_size = raw_chain_size; + + /* Update output size. */ + *out_size = chain.size; end: certFreeCertificateChain(&chain); @@ -138,34 +139,40 @@ u8 *certRetrieveRawCertificateChainFromGameCardByRightsId(const FsRightsId *id, u8 *raw_chain = NULL; bool success = false; + /* Generate certificate chain filename. */ utilsGenerateHexStringFromData(raw_chain_filename, sizeof(raw_chain_filename), id->c, sizeof(id->c), false); strcat(raw_chain_filename, ".cert"); + /* Get certificate chain entry info. */ if (!gamecardGetHashFileSystemEntryInfoByName(HashFileSystemPartitionType_Secure, raw_chain_filename, &raw_chain_offset, &raw_chain_size)) { LOG_MSG_ERROR("Error retrieving offset and size for \"%s\" entry in secure hash FS partition!", raw_chain_filename); - return NULL; + goto end; } + /* Validate certificate chain size. */ if (raw_chain_size < SIGNED_CERT_MIN_SIZE) { LOG_MSG_ERROR("Invalid size for \"%s\"! (0x%lX).", raw_chain_filename, raw_chain_size); - return NULL; + goto end; } + /* Allocate memory for the certificate chain. */ raw_chain = malloc(raw_chain_size); if (!raw_chain) { LOG_MSG_ERROR("Unable to allocate memory for raw \"%s\" certificate chain! (0x%lX).", raw_chain_filename, raw_chain_size); - return NULL; + goto end; } + /* Read raw certificate chain from the inserted gamecard. */ if (!gamecardReadStorage(raw_chain, raw_chain_size, raw_chain_offset)) { LOG_MSG_ERROR("Failed to read \"%s\" data from the inserted gamecard!", raw_chain_filename); goto end; } + /* Update output. */ *out_size = raw_chain_size; success = true; @@ -202,9 +209,9 @@ static void certCloseEsCertSaveFile(void) static bool _certRetrieveCertificateByName(Certificate *dst, const char *name) { - if (!g_esCertSaveCtx) + if (!g_esCertSaveCtx || !dst || !name || !*name) { - LOG_MSG_ERROR("ES certificate savefile not opened!"); + LOG_MSG_ERROR("Invalid parameters!"); return false; } @@ -212,22 +219,27 @@ static bool _certRetrieveCertificateByName(Certificate *dst, const char *name) char cert_path[SAVE_FS_LIST_MAX_NAME_LENGTH] = {0}; allocation_table_storage_ctx_t fat_storage = {0}; - snprintf(cert_path, SAVE_FS_LIST_MAX_NAME_LENGTH, CERT_SAVEFILE_STORAGE_BASE_PATH "%s", name); + /* Generate path for the requested certificate within the ES certificate save file. */ + snprintf(cert_path, MAX_ELEMENTS(cert_path), CERT_SAVEFILE_STORAGE_BASE_PATH "%s", name); + /* Get FAT storage info for the certificate. */ if (!save_get_fat_storage_from_file_entry_by_path(g_esCertSaveCtx, cert_path, &fat_storage, &cert_size)) { LOG_MSG_ERROR("Failed to locate certificate \"%s\" in ES certificate system save!", name); return false; } + /* Validate certificate size. */ if (cert_size < SIGNED_CERT_MIN_SIZE || cert_size > SIGNED_CERT_MAX_SIZE) { LOG_MSG_ERROR("Invalid size for certificate \"%s\"! (0x%lX).", name, cert_size); return false; } + /* Update output size. */ dst->size = cert_size; + /* Read certificate data. */ u64 br = save_allocation_table_storage_read(&fat_storage, dst->data, 0, dst->size); if (br != dst->size) { @@ -235,8 +247,9 @@ static bool _certRetrieveCertificateByName(Certificate *dst, const char *name) return false; } + /* Get certificate type. */ dst->type = certGetCertificateType(dst->data, dst->size); - if (dst->type == CertType_None) + if (dst->type == CertType_None || dst->type >= CertType_Count) { LOG_MSG_ERROR("Invalid certificate type for \"%s\"!", name); return false; @@ -247,26 +260,26 @@ static bool _certRetrieveCertificateByName(Certificate *dst, const char *name) static u8 certGetCertificateType(void *data, u64 data_size) { - CertCommonBlock *cert_common_block = NULL; - u32 sig_type = 0, pub_key_type = 0; - u64 signed_cert_size = 0; - u8 type = CertType_None; - if (!data || data_size < SIGNED_CERT_MIN_SIZE || data_size > SIGNED_CERT_MAX_SIZE) { LOG_MSG_ERROR("Invalid parameters!"); - return type; + return CertType_None; } - if (!(cert_common_block = certGetCommonBlock(data)) || !(signed_cert_size = certGetSignedCertificateSize(data)) || signed_cert_size > data_size) + u32 sig_type = 0, pub_key_type = 0; + u8 type = CertType_None; + + /* Get signature and public key types. */ + sig_type = signatureGetTypeFromSignedBlob(data, true); + pub_key_type = certGetPublicKeyTypeFromSignedCertBlob(data); + + if (!signatureIsValidType(sig_type) || !certIsValidPublicKeyType(pub_key_type)) { LOG_MSG_ERROR("Input buffer doesn't hold a valid signed certificate!"); - return type; + goto end; } - sig_type = signatureGetSigType(data, true); - pub_key_type = __builtin_bswap32(cert_common_block->pub_key_type); - + /* Determine certificate type. */ switch(sig_type) { case SignatureType_Rsa4096Sha1: @@ -288,53 +301,67 @@ static u8 certGetCertificateType(void *data, u64 data_size) break; } +end: return type; } static bool _certRetrieveCertificateChainBySignatureIssuer(CertificateChain *dst, const char *issuer) { - if (!g_esCertSaveCtx) + if (!dst || !issuer || !*issuer) { - LOG_MSG_ERROR("ES certificate savefile not opened!"); + LOG_MSG_ERROR("Invalid parameters!"); return false; } u32 i = 0; char issuer_copy[0x40] = {0}, *pch = NULL, *state = NULL; - bool success = true; + bool success = false; + /* Free output context beforehand. */ + certFreeCertificateChain(dst); + + /* Determine how many certificate we need to retrieve. */ dst->count = certGetCertificateCountInSignatureIssuer(issuer); if (!dst->count) { LOG_MSG_ERROR("Invalid signature issuer string!"); - return false; + goto end; } + /* Allocate memory for all the certificates. */ dst->certs = calloc(dst->count, sizeof(Certificate)); if (!dst->certs) { LOG_MSG_ERROR("Unable to allocate memory for the certificate chain! (0x%lX).", dst->count * sizeof(Certificate)); - return false; + goto end; } /* Copy string to avoid problems with strtok_r(). */ /* The "Root-" parent from the issuer string is skipped. */ snprintf(issuer_copy, sizeof(issuer_copy), "%s", issuer + 5); + /* Collect all required certificates. */ pch = strtok_r(issuer_copy, "-", &state); while(pch) { + /* Get current certificate. */ if (!_certRetrieveCertificateByName(&(dst->certs[i]), pch)) { LOG_MSG_ERROR("Unable to retrieve certificate \"%s\"!", pch); - success = false; - break; + goto end; } - i++; + /* Update certificate chain size and current index. */ + dst->size += dst->certs[i++].size; + + /* Get name for the next certificate. */ pch = strtok_r(NULL, "-", &state); } + /* Update output value. */ + success = true; + +end: if (!success) certFreeCertificateChain(dst); return success; @@ -361,15 +388,6 @@ static u32 certGetCertificateCountInSignatureIssuer(const char *issuer) return count; } -static u64 certCalculateRawCertificateChainSize(const CertificateChain *chain) -{ - if (!chain || !chain->count || !chain->certs) return 0; - - u64 chain_size = 0; - for(u32 i = 0; i < chain->count; i++) chain_size += chain->certs[i].size; - return chain_size; -} - static void certCopyCertificateChainDataToMemoryBuffer(void *dst, const CertificateChain *chain) { if (!chain || !chain->count || !chain->certs) return; diff --git a/source/core/keys.c b/source/core/keys.c index 7057c49..9168e6b 100644 --- a/source/core/keys.c +++ b/source/core/keys.c @@ -194,7 +194,7 @@ const u8 *keysGetNcaKeyAreaEncryptionKey(u8 kaek_index, u8 key_generation) goto end; } - if (key_generation >= NcaKeyGeneration_Max) + if (key_generation > NcaKeyGeneration_Max) { LOG_MSG_ERROR("Invalid key generation value! (0x%02X).", key_generation); goto end; @@ -258,7 +258,7 @@ const u8 *keysGetTicketCommonKey(u8 key_generation) const u8 *ret = NULL; u8 key_gen_val = (key_generation ? (key_generation - 1) : key_generation); - if (key_generation >= NcaKeyGeneration_Max) + if (key_generation > NcaKeyGeneration_Max) { LOG_MSG_ERROR("Invalid key generation value! (0x%02X).", key_generation); goto end; diff --git a/source/core/nca.c b/source/core/nca.c index b770c90..1f9820b 100644 --- a/source/core/nca.c +++ b/source/core/nca.c @@ -244,12 +244,13 @@ bool ncaInitializeContext(NcaContext *out, u8 storage_id, u8 hfs_partition_type, /* Retrieve ticket. */ /* This will return true if it has already been retrieved. */ - if (tikRetrieveTicketByRightsId(usable_tik, &(out->header.rights_id), out->storage_id == NcmStorageId_GameCard)) + if (tikRetrieveTicketByRightsId(usable_tik, &(out->header.rights_id), out->key_generation, out->storage_id == NcmStorageId_GameCard)) { /* Copy decrypted titlekey. */ - memcpy(out->titlekey, usable_tik->dec_titlekey, 0x10); + memcpy(out->titlekey, usable_tik->dec_titlekey, sizeof(usable_tik->dec_titlekey)); out->titlekey_retrieved = true; } else { + /* We must proceed even if we have no ticket. The user may just want to copy a raw NCA. */ LOG_MSG_ERROR("Error retrieving ticket for NCA \"%s\"!", out->content_id_str); } } diff --git a/source/core/rsa.c b/source/core/rsa.c index 897268a..9b9074a 100644 --- a/source/core/rsa.c +++ b/source/core/rsa.c @@ -28,47 +28,19 @@ #include #include +/* Function prototypes. */ + +static bool rsa2048VerifySha256BasedSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, \ + bool use_pss); + bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size) { - if (!data || !data_size || !signature || !modulus || !public_exponent || !public_exponent_size) - { - LOG_MSG_ERROR("Invalid parameters!"); - return false; - } + return rsa2048VerifySha256BasedSignature(data, data_size, signature, modulus, public_exponent, public_exponent_size, true); +} - int mbedtls_ret = 0; - mbedtls_rsa_context rsa; - u8 hash[SHA256_HASH_SIZE] = {0}; - bool ret = false; - - /* Initialize RSA context. */ - mbedtls_rsa_init(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256); - - /* Import RSA parameters. */ - mbedtls_ret = mbedtls_rsa_import_raw(&rsa, (const u8*)modulus, RSA2048_BYTES, NULL, 0, NULL, 0, NULL, 0, (const u8*)public_exponent, public_exponent_size); - if (mbedtls_ret != 0) - { - LOG_MSG_ERROR("mbedtls_rsa_import_raw failed! (%d).", mbedtls_ret); - goto end; - } - - /* Calculate SHA-256 checksum for the input data. */ - sha256CalculateHash(hash, data, data_size); - - /* Verify signature. */ - mbedtls_ret = mbedtls_rsa_rsassa_pss_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, SHA256_HASH_SIZE, hash, (const u8*)signature); - if (mbedtls_ret != 0) - { - LOG_MSG_ERROR("mbedtls_rsa_rsassa_pss_verify failed! (%d).", mbedtls_ret); - goto end; - } - - ret = true; - -end: - mbedtls_rsa_free(&rsa); - - return ret; +bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size) +{ + return rsa2048VerifySha256BasedSignature(data, data_size, signature, modulus, public_exponent, public_exponent_size, false); } bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \ @@ -135,3 +107,48 @@ end: return ret; } + +static bool rsa2048VerifySha256BasedSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, \ + bool use_pss) +{ + if (!data || !data_size || !signature || !modulus || !public_exponent || !public_exponent_size) + { + LOG_MSG_ERROR("Invalid parameters!"); + return false; + } + + int mbedtls_ret = 0; + mbedtls_rsa_context rsa; + u8 hash[SHA256_HASH_SIZE] = {0}; + bool ret = false; + + /* Initialize RSA context. */ + mbedtls_rsa_init(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256); + + /* Import RSA parameters. */ + mbedtls_ret = mbedtls_rsa_import_raw(&rsa, (const u8*)modulus, RSA2048_BYTES, NULL, 0, NULL, 0, NULL, 0, (const u8*)public_exponent, public_exponent_size); + if (mbedtls_ret != 0) + { + LOG_MSG_ERROR("mbedtls_rsa_import_raw failed! (%d).", mbedtls_ret); + goto end; + } + + /* Calculate SHA-256 checksum for the input data. */ + sha256CalculateHash(hash, data, data_size); + + /* Verify signature. */ + mbedtls_ret = (use_pss ? mbedtls_rsa_rsassa_pss_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, SHA256_HASH_SIZE, hash, (const u8*)signature) : \ + mbedtls_rsa_rsassa_pkcs1_v15_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, SHA256_HASH_SIZE, hash, (const u8*)signature)); + if (mbedtls_ret != 0) + { + LOG_MSG_ERROR("mbedtls_rsa_rsassa_%s_verify failed! (%d).", use_pss ? "pss" : "pkcs1_v15", mbedtls_ret); + goto end; + } + + ret = true; + +end: + mbedtls_rsa_free(&rsa); + + return ret; +} diff --git a/source/core/tik.c b/source/core/tik.c index 1c33b83..a30e2d6 100644 --- a/source/core/tik.c +++ b/source/core/tik.c @@ -29,6 +29,7 @@ #include "gamecard.h" #include "mem.h" #include "aes.h" +#include "rsa.h" #define TIK_COMMON_SAVEFILE_PATH BIS_SYSTEM_PARTITION_MOUNT_NAME "/save/80000000000000e1" #define TIK_PERSONALIZED_SAVEFILE_PATH BIS_SYSTEM_PARTITION_MOUNT_NAME "/save/80000000000000e2" @@ -36,6 +37,9 @@ #define TIK_LIST_STORAGE_PATH "/ticket_list.bin" #define TIK_DB_STORAGE_PATH "/ticket.bin" +#define TIK_COMMON_CERT_NAME "XS00000020" +#define TIK_DEV_CERT_ISSUER "CA00000004" + /* Type definitions. */ /// Used to parse ticket_list.bin entries. @@ -70,6 +74,8 @@ NXDT_ASSERT(TikEsCtrKeyPattern9x, 0x28); /* Global variables. */ +static Mutex g_esTikSaveMutex = 0; + #if LOG_LEVEL <= LOG_LEVEL_ERROR static const char *g_tikTitleKeyTypeStrings[] = { [TikTitleKeyType_Common] = "common", @@ -89,73 +95,96 @@ static MemoryLocation g_esMemoryLocation = { static bool tikRetrieveTicketFromGameCardByRightsId(Ticket *dst, const FsRightsId *id); static bool tikRetrieveTicketFromEsSaveDataByRightsId(Ticket *dst, const FsRightsId *id); -static bool tikGetEncryptedTitleKeyFromTicket(Ticket *tik); +static bool tikFixTamperedCommonTicket(Ticket *tik); +static bool tikVerifyRsa2048Sha256Signature(const TikCommonBlock *tik_common_block, u64 hash_area_size, const u8 *signature); + +static bool tikGetEncryptedTitleKey(Ticket *tik); static bool tikGetDecryptedTitleKey(void *dst, const void *src, u8 key_generation); static bool tikGetTitleKeyTypeFromRightsId(const FsRightsId *id, u8 *out); static bool tikRetrieveRightsIdsByTitleKeyType(FsRightsId **out, u32 *out_count, bool personalized); -static bool tikGetTicketEntryOffsetFromTicketList(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 *out_offset, u8 titlekey_type); -static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 ticket_offset, u8 titlekey_type); +static bool tikGetTicketEntryOffsetFromTicketList(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u8 titlekey_type, u64 *out_offset); +static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u8 titlekey_type, u64 ticket_offset); +static bool tikDecryptVolatileTicket(u8 *buf, u64 ticket_offset); static bool tikGetTicketTypeAndSize(void *data, u64 data_size, u8 *out_type, u64 *out_size); -bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, bool use_gamecard) +bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, u8 key_generation, bool use_gamecard) { - if (!dst || !id) + if (!dst || !id || key_generation > NcaKeyGeneration_Max) { LOG_MSG_ERROR("Invalid parameters!"); return false; } - u8 key_generation = id->c[0xF]; TikCommonBlock *tik_common_block = NULL; + bool success = false, tik_retrieved = false; /* Check if this ticket has already been retrieved. */ - if (dst->type > TikType_None && dst->type <= TikType_SigHmac160 && dst->size >= SIGNED_TIK_MIN_SIZE && dst->size <= SIGNED_TIK_MAX_SIZE) + tik_common_block = tikGetCommonBlockFromTicket(dst); + if (tik_common_block && !memcmp(tik_common_block->rights_id.c, id->c, sizeof(id->c))) { - tik_common_block = tikGetCommonBlock(dst->data); - if (tik_common_block && !memcmp(tik_common_block->rights_id.c, id->c, 0x10)) return true; + success = true; + goto end; } /* Clear output ticket. */ memset(dst, 0, sizeof(Ticket)); + /* Validate the key generation field within the rights ID. */ + u8 key_gen_rid = id->c[0xF]; + bool old_key_gen = (key_generation < NcaKeyGeneration_Since301NUP); + + if ((old_key_gen && key_gen_rid) || (!old_key_gen && key_gen_rid != key_generation)) + { + LOG_MSG_ERROR("Invalid rights ID key generation! Got 0x%02X, expected 0x%02X.", key_gen_rid, old_key_gen ? 0 : key_generation); + goto end; + } + + /* Update key generation field. */ + dst->key_generation = key_generation; + /* Retrieve ticket data. */ - bool tik_retrieved = (use_gamecard ? tikRetrieveTicketFromGameCardByRightsId(dst, id) : tikRetrieveTicketFromEsSaveDataByRightsId(dst, id)); + if (use_gamecard) + { + tik_retrieved = tikRetrieveTicketFromGameCardByRightsId(dst, id); + } else { + SCOPED_LOCK(&g_esTikSaveMutex) tik_retrieved = tikRetrieveTicketFromEsSaveDataByRightsId(dst, id); + } + if (!tik_retrieved) { LOG_MSG_ERROR("Unable to retrieve ticket data!"); - return false; + goto end; } - /* Get encrypted titlekey from ticket. */ - if (!tikGetEncryptedTitleKeyFromTicket(dst)) + /* Fix tampered common ticket, if needed. */ + if (!tikFixTamperedCommonTicket(dst)) goto end; + + /* Get encrypted titlekey. */ + if (!tikGetEncryptedTitleKey(dst)) { LOG_MSG_ERROR("Unable to retrieve encrypted titlekey from ticket!"); - return false; + goto end; } - /* Get common ticket block. */ - tik_common_block = tikGetCommonBlock(dst->data); - - /* Get proper key generation value. */ - /* Nintendo didn't start putting the key generation value into the rights ID until HOS 3.0.1. */ - /* If this is the case, we'll just use the key generation value from the common ticket block. */ - /* However, old custom tools used to wipe the key generation field or save its value to a different offset, so this may fail with titles with custom/modified tickets. */ - if (key_generation < NcaKeyGeneration_Since301NUP || key_generation > NcaKeyGeneration_Max) key_generation = tik_common_block->key_generation; - /* Get decrypted titlekey. */ - if (!tikGetDecryptedTitleKey(dst->dec_titlekey, dst->enc_titlekey, key_generation)) + if (!(success = tikGetDecryptedTitleKey(dst->dec_titlekey, dst->enc_titlekey, dst->key_generation))) { LOG_MSG_ERROR("Unable to decrypt titlekey!"); - return false; + goto end; } - /* Generate rights ID string. */ + /* Generate hex strings. */ + tik_common_block = tikGetCommonBlockFromSignedTicketBlob(dst->data); + + utilsGenerateHexStringFromData(dst->enc_titlekey_str, sizeof(dst->enc_titlekey_str), dst->enc_titlekey, sizeof(dst->enc_titlekey), false); + utilsGenerateHexStringFromData(dst->dec_titlekey_str, sizeof(dst->dec_titlekey_str), dst->dec_titlekey, sizeof(dst->dec_titlekey), false); utilsGenerateHexStringFromData(dst->rights_id_str, sizeof(dst->rights_id_str), tik_common_block->rights_id.c, sizeof(tik_common_block->rights_id.c), false); - return true; +end: + return success; } bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_chain, u64 *out_raw_cert_chain_size) @@ -168,29 +197,22 @@ bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_c bool dev_cert = false; char cert_chain_issuer[0x40] = {0}; - static const char *common_cert_names[] = { "XS00000020", "XS00000022", NULL }; u8 *raw_cert_chain = NULL; u64 raw_cert_chain_size = 0; - if (!tik || tik->type == TikType_None || tik->type > TikType_SigHmac160 || tik->size < SIGNED_TIK_MIN_SIZE || tik->size > SIGNED_TIK_MAX_SIZE || \ - !(tik_common_block = tikGetCommonBlock(tik->data)) || tik_common_block->titlekey_type != TikTitleKeyType_Personalized || (!out_raw_cert_chain && out_raw_cert_chain_size) || \ - (out_raw_cert_chain && !out_raw_cert_chain_size)) + if (!(tik_common_block = tikGetCommonBlockFromTicket(tik)) || tik_common_block->titlekey_type != TikTitleKeyType_Personalized || \ + (!out_raw_cert_chain && out_raw_cert_chain_size) || (out_raw_cert_chain && !out_raw_cert_chain_size)) { LOG_MSG_ERROR("Invalid parameters!"); return false; } /* Generate raw certificate chain for the new signature issuer (common). */ - dev_cert = (strstr(tik_common_block->issuer, "CA00000004") != NULL); - - for(u8 i = 0; common_cert_names[i] != NULL; i++) - { - sprintf(cert_chain_issuer, "Root-CA%08X-%s", dev_cert ? 4 : 3, common_cert_names[i]); - raw_cert_chain = certGenerateRawCertificateChainBySignatureIssuer(cert_chain_issuer, &raw_cert_chain_size); - if (raw_cert_chain) break; - } + dev_cert = (strstr(tik_common_block->issuer, TIK_DEV_CERT_ISSUER) != NULL); + sprintf(cert_chain_issuer, "Root-CA%08X-%s", dev_cert ? 4 : 3, TIK_COMMON_CERT_NAME); + raw_cert_chain = certGenerateRawCertificateChainBySignatureIssuer(cert_chain_issuer, &raw_cert_chain_size); if (!raw_cert_chain) { LOG_MSG_ERROR("Failed to generate raw certificate chain for common ticket signature issuer!"); @@ -198,9 +220,9 @@ bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_c } /* Wipe signature. */ - sig_type = signatureGetSigType(tik->data, false); - signature = signatureGetSig(tik->data); - signature_size = signatureGetSigSize(sig_type); + sig_type = signatureGetTypeFromSignedBlob(tik->data, false); + signature = signatureGetSigFromSignedBlob(tik->data); + signature_size = signatureGetSigSizeByType(sig_type); memset(signature, 0xFF, signature_size); /* Change signature issuer. */ @@ -209,14 +231,16 @@ bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_c /* Wipe the titlekey block and copy the encrypted titlekey to it. */ memset(tik_common_block->titlekey_block, 0, sizeof(tik_common_block->titlekey_block)); - memcpy(tik_common_block->titlekey_block, tik->enc_titlekey, 0x10); + memcpy(tik_common_block->titlekey_block, tik->enc_titlekey, sizeof(tik->enc_titlekey)); /* Update ticket size. */ - tik->size = (signatureGetBlockSize(sig_type) + sizeof(TikCommonBlock)); + tik->size = (signatureGetBlockSizeByType(sig_type) + sizeof(TikCommonBlock)); /* Update the rest of the ticket fields. */ tik_common_block->titlekey_type = TikTitleKeyType_Common; - tik_common_block->property_mask &= ~(TikPropertyMask_ELicenseRequired | TikPropertyMask_Volatile); + tik_common_block->license_type = TikLicenseType_Permanent; + tik_common_block->property_mask = TikPropertyMask_None; + tik_common_block->ticket_id = 0; tik_common_block->device_id = 0; tik_common_block->account_id = 0; @@ -251,35 +275,37 @@ static bool tikRetrieveTicketFromGameCardByRightsId(Ticket *dst, const FsRightsI char tik_filename[0x30] = {0}; u64 tik_offset = 0, tik_size = 0; + bool success = false; utilsGenerateHexStringFromData(tik_filename, sizeof(tik_filename), id->c, sizeof(id->c), false); strcat(tik_filename, ".tik"); + /* Get ticket entry info. */ if (!gamecardGetHashFileSystemEntryInfoByName(HashFileSystemPartitionType_Secure, tik_filename, &tik_offset, &tik_size)) { LOG_MSG_ERROR("Error retrieving offset and size for \"%s\" entry in secure hash FS partition!", tik_filename); - return false; + goto end; } + /* Validate ticket size. */ if (tik_size < SIGNED_TIK_MIN_SIZE || tik_size > SIGNED_TIK_MAX_SIZE) { LOG_MSG_ERROR("Invalid size for \"%s\"! (0x%lX).", tik_filename, tik_size); - return false; + goto end; } + /* Read ticket data. */ if (!gamecardReadStorage(dst->data, tik_size, tik_offset)) { LOG_MSG_ERROR("Failed to read \"%s\" data from the inserted gamecard!", tik_filename); - return false; + goto end; } - if (!tikGetTicketTypeAndSize(dst->data, tik_size, &(dst->type), &(dst->size))) - { - LOG_MSG_ERROR("Unable to determine ticket type and size!"); - return false; - } + /* Get ticket type and size. */ + if (!(success = tikGetTicketTypeAndSize(dst->data, tik_size, &(dst->type), &(dst->size)))) LOG_MSG_ERROR("Unable to determine ticket type and size!"); - return true; +end: + return success; } static bool tikRetrieveTicketFromEsSaveDataByRightsId(Ticket *dst, const FsRightsId *id) @@ -304,7 +330,7 @@ static bool tikRetrieveTicketFromEsSaveDataByRightsId(Ticket *dst, const FsRight if (!(buf = malloc(buf_size))) { LOG_MSG_ERROR("Unable to allocate 0x%lX bytes block for temporary read buffer!", buf_size); - return false; + goto end; } /* Get titlekey type. */ @@ -322,30 +348,29 @@ static bool tikRetrieveTicketFromEsSaveDataByRightsId(Ticket *dst, const FsRight } /* Get ticket entry offset from ticket_list.bin. */ - if (!tikGetTicketEntryOffsetFromTicketList(save_ctx, buf, buf_size, id, &ticket_offset, titlekey_type)) + if (!tikGetTicketEntryOffsetFromTicketList(save_ctx, buf, buf_size, id, titlekey_type, &ticket_offset)) { LOG_MSG_ERROR("Unable to find an entry with a matching Rights ID in \"%s\" from ES %s ticket system save!", TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); goto end; } /* Get ticket entry from ticket.bin. */ - if (!tikRetrieveTicketEntryFromTicketBin(save_ctx, buf, buf_size, id, ticket_offset, titlekey_type)) + if (!tikRetrieveTicketEntryFromTicketBin(save_ctx, buf, buf_size, id, titlekey_type, ticket_offset)) { LOG_MSG_ERROR("Unable to find a matching %s ticket entry for the provided Rights ID!", g_tikTitleKeyTypeStrings[titlekey_type]); goto end; } /* Get ticket type and size. */ - if (!tikGetTicketTypeAndSize(buf, SIGNED_TIK_MAX_SIZE, &(dst->type), &(dst->size))) + if (!(success = tikGetTicketTypeAndSize(buf, SIGNED_TIK_MAX_SIZE, &(dst->type), &(dst->size)))) { LOG_MSG_ERROR("Unable to determine ticket type and size!"); goto end; } + /* Copy ticket data. */ memcpy(dst->data, buf, dst->size); - success = true; - end: if (save_ctx) save_close_savefile(save_ctx); @@ -354,33 +379,121 @@ end: return success; } -static bool tikGetEncryptedTitleKeyFromTicket(Ticket *tik) +static bool tikFixTamperedCommonTicket(Ticket *tik) { TikCommonBlock *tik_common_block = NULL; - if (!tik || !(tik_common_block = tikGetCommonBlock(tik->data))) + u32 sig_type = 0; + u8 *signature = NULL; + u64 signature_size = 0, hash_area_size = 0; + + bool success = false; + + if (!tik || tik->key_generation > NcaKeyGeneration_Max || !(tik_common_block = tikGetCommonBlockFromSignedTicketBlob(tik->data))) { LOG_MSG_ERROR("Invalid parameters!"); return false; } + /* Get ticket signature and its properties, as well as the ticket hash area size. */ + sig_type = signatureGetTypeFromSignedBlob(tik->data, false); + signature = signatureGetSigFromSignedBlob(tik->data); + signature_size = signatureGetSigSizeByType(sig_type); + hash_area_size = tikGetSignedTicketBlobHashAreaSize(tik->data); + + /* Return right away if we're not dealing with a common ticket, if the signature type doesn't match RSA-2048 + SHA-256, or if the signature is valid. */ + if (tik_common_block->titlekey_type != TikTitleKeyType_Common || sig_type != SignatureType_Rsa2048Sha256 || \ + tikVerifyRsa2048Sha256Signature(tik_common_block, hash_area_size, signature)) + { + success = true; + goto end; + } + + LOG_MSG_DEBUG("Detected tampered common ticket!"); + + /* Nintendo didn't start putting the key generation value into the rights ID until HOS 3.0.1. */ + /* Old custom tools used to wipe the key generation field and/or save its value into a different offset. */ + /* We're gonna take care of that by setting the correct values where they need to go. */ + memset(signature, 0xFF, signature_size); + + tik_common_block->titlekey_type = TikTitleKeyType_Common; + tik_common_block->license_type = TikLicenseType_Permanent; + tik_common_block->key_generation = tik->key_generation; + tik_common_block->property_mask = TikPropertyMask_None; + + tik_common_block->ticket_id = 0; + tik_common_block->device_id = 0; + tik_common_block->account_id = 0; + + tik_common_block->sect_total_size = 0; + tik_common_block->sect_hdr_offset = (u32)tik->size; + tik_common_block->sect_hdr_count = 0; + tik_common_block->sect_hdr_entry_size = 0; + + /* Update return value. */ + success = true; + +end: + return success; +} + +static bool tikVerifyRsa2048Sha256Signature(const TikCommonBlock *tik_common_block, u64 hash_area_size, const u8 *signature) +{ + if (!tik_common_block || hash_area_size < sizeof(TikCommonBlock) || hash_area_size > (SIGNED_TIK_MAX_SIZE - sizeof(SignatureBlockRsa2048)) || !signature) + { + LOG_MSG_ERROR("Invalid parameters!"); + return false; + } + + const char *cert_name = (strrchr(tik_common_block->issuer, '-') + 1); + Certificate cert = {0}; + const u8 *modulus = NULL, *public_exponent = NULL; + + /* Get certificate for the ticket signature issuer. */ + if (!certRetrieveCertificateByName(&cert, cert_name)) + { + LOG_MSG_ERROR("Failed to retrieve certificate for \"%s\".", cert_name); + return false; + } + + /* Get certificate modulus and public exponent. */ + modulus = certGetPublicKeyFromCertificate(&cert); + public_exponent = certGetPublicExponentFromCertificate(&cert); + + /* Validate the ticket signature. */ + return rsa2048VerifySha256BasedPkcs1v15Signature(tik_common_block, hash_area_size, signature, modulus, public_exponent, CERT_RSA_PUB_EXP_SIZE); +} + +static bool tikGetEncryptedTitleKey(Ticket *tik) +{ + TikCommonBlock *tik_common_block = NULL; + + if (!tik || !(tik_common_block = tikGetCommonBlockFromSignedTicketBlob(tik->data))) + { + LOG_MSG_ERROR("Invalid parameters!"); + return false; + } + + bool success = false; + switch(tik_common_block->titlekey_type) { case TikTitleKeyType_Common: /* No console-specific crypto used. Copy encrypted titlekey right away. */ - memcpy(tik->enc_titlekey, tik_common_block->titlekey_block, 0x10); + memcpy(tik->enc_titlekey, tik_common_block->titlekey_block, sizeof(tik->enc_titlekey)); + success = true; break; case TikTitleKeyType_Personalized: /* The titlekey block is encrypted using RSA-OAEP with a console-specific RSA key. */ /* We have to perform a RSA-OAEP unwrap operation to get the encrypted titlekey. */ - if (!keysDecryptRsaOaepWrappedTitleKey(tik_common_block->titlekey_block, tik->enc_titlekey)) return false; + success = keysDecryptRsaOaepWrappedTitleKey(tik_common_block->titlekey_block, tik->enc_titlekey); break; default: LOG_MSG_ERROR("Invalid titlekey type value! (0x%02X).", tik_common_block->titlekey_type); - return false; + break; } - return true; + return success; } static bool tikGetDecryptedTitleKey(void *dst, const void *src, u8 key_generation) @@ -417,30 +530,27 @@ static bool tikGetTitleKeyTypeFromRightsId(const FsRightsId *id, u8 *out) FsRightsId *rights_ids = NULL; bool found = false; - for(u8 i = 0; i < 2; i++) + for(u8 i = TikTitleKeyType_Common; i < TikTitleKeyType_Count; i++) { - count = 0; - rights_ids = NULL; - - if (!tikRetrieveRightsIdsByTitleKeyType(&rights_ids, &count, i == 1)) + /* Get all rights IDs for the current titlekey type. */ + if (!tikRetrieveRightsIdsByTitleKeyType(&rights_ids, &count, i == TikTitleKeyType_Personalized)) { LOG_MSG_ERROR("Unable to retrieve %s rights IDs!", g_tikTitleKeyTypeStrings[i]); continue; } - if (!count) continue; - + /* Look for the provided rights ID. */ for(u32 j = 0; j < count; j++) { - if (!memcmp(rights_ids[j].c, id->c, 0x10)) + if (!memcmp(rights_ids[j].c, id->c, sizeof(id->c))) { - *out = i; /* TikTitleKeyType_Common or TikTitleKeyType_Personalized. */ + *out = i; found = true; break; } } - free(rights_ids); + if (rights_ids) free(rights_ids); if (found) break; } @@ -459,6 +569,7 @@ static bool tikRetrieveRightsIdsByTitleKeyType(FsRightsId **out, u32 *out_count, Result rc = 0; u32 count = 0, ids_written = 0; FsRightsId *rights_ids = NULL; + bool success = false; #if LOG_LEVEL <= LOG_LEVEL_ERROR u8 str_idx = (personalized ? TikTitleKeyType_Personalized : TikTitleKeyType_Common); @@ -467,43 +578,51 @@ static bool tikRetrieveRightsIdsByTitleKeyType(FsRightsId **out, u32 *out_count, *out = NULL; *out_count = 0; + /* Get ticket count for the provided titlekey type. */ rc = (personalized ? esCountPersonalizedTicket((s32*)&count) : esCountCommonTicket((s32*)&count)); if (R_FAILED(rc)) { LOG_MSG_ERROR("esCount%c%sTicket failed! (0x%X).", toupper(g_tikTitleKeyTypeStrings[str_idx][0]), g_tikTitleKeyTypeStrings[str_idx] + 1, rc); - return false; + goto end; } if (!count) { - LOG_MSG_ERROR("No %s tickets available!", g_tikTitleKeyTypeStrings[str_idx]); - return true; + LOG_MSG_WARNING("No %s tickets available!", g_tikTitleKeyTypeStrings[str_idx]); + success = true; + goto end; } + /* Allocate memory for our rights ID array. */ rights_ids = calloc(count, sizeof(FsRightsId)); if (!rights_ids) { LOG_MSG_ERROR("Unable to allocate memory for %s rights IDs!", g_tikTitleKeyTypeStrings[str_idx]); - return false; + goto end; } + /* Get rights IDs from all tickets that match the provided titlekey type. */ rc = (personalized ? esListPersonalizedTicket((s32*)&ids_written, rights_ids, (s32)count) : esListCommonTicket((s32*)&ids_written, rights_ids, (s32)count)); - if (R_FAILED(rc) || !ids_written) + success = (R_SUCCEEDED(rc) && ids_written); + if (!success) { LOG_MSG_ERROR("esList%c%sTicket failed! (0x%X). Wrote %u entries, expected %u entries.", toupper(g_tikTitleKeyTypeStrings[str_idx][0]), g_tikTitleKeyTypeStrings[str_idx] + 1, rc, ids_written, count); - free(rights_ids); - return false; + goto end; } + /* Update output values. */ *out = rights_ids; *out_count = ids_written; - return true; +end: + if (!success && rights_ids) free(rights_ids); + + return success; } -static bool tikGetTicketEntryOffsetFromTicketList(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 *out_offset, u8 titlekey_type) +static bool tikGetTicketEntryOffsetFromTicketList(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u8 titlekey_type, u64 *out_offset) { - if (!save_ctx || !buf || !buf_size || (buf_size % sizeof(TikListEntry)) != 0 || !id || !out_offset) + if (!save_ctx || !buf || !buf_size || (buf_size % sizeof(TikListEntry)) != 0 || !id || titlekey_type >= TikTitleKeyType_Count || !out_offset) { LOG_MSG_ERROR("Invalid parameters!"); return false; @@ -512,7 +631,7 @@ static bool tikGetTicketEntryOffsetFromTicketList(save_ctx_t *save_ctx, u8 *buf, allocation_table_storage_ctx_t fat_storage = {0}; u64 ticket_list_bin_size = 0, br = 0, total_br = 0; - u8 last_rights_id[0x10]; + u8 last_rights_id[0x10] = {0}; memset(last_rights_id, 0xFF, sizeof(last_rights_id)); bool last_entry_found = false, success = false; @@ -521,27 +640,30 @@ static bool tikGetTicketEntryOffsetFromTicketList(save_ctx_t *save_ctx, u8 *buf, if (!save_get_fat_storage_from_file_entry_by_path(save_ctx, TIK_LIST_STORAGE_PATH, &fat_storage, &ticket_list_bin_size)) { LOG_MSG_ERROR("Failed to locate \"%s\" in ES %s ticket system save!", TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); - return false; + goto end; } - /* Check ticket_list.bin size. */ + /* Validate ticket_list.bin size. */ if (ticket_list_bin_size < sizeof(TikListEntry) || (ticket_list_bin_size % sizeof(TikListEntry)) != 0) { LOG_MSG_ERROR("Invalid size for \"%s\" in ES %s ticket system save! (0x%lX).", TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type], ticket_list_bin_size); - return false; + goto end; } /* Look for an entry matching our rights ID in ticket_list.bin. */ while(total_br < ticket_list_bin_size) { + /* Update chunk size, if needed. */ if (buf_size > (ticket_list_bin_size - total_br)) buf_size = (ticket_list_bin_size - total_br); + /* Read current chunk. */ if ((br = save_allocation_table_storage_read(&fat_storage, buf, total_br, buf_size)) != buf_size) { LOG_MSG_ERROR("Failed to read 0x%lX bytes chunk at offset 0x%lX from \"%s\" in ES %s ticket system save!", buf_size, total_br, TIK_LIST_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); break; } + /* Process individual ticket list entries. */ for(u64 i = 0; i < buf_size; i += sizeof(TikListEntry)) { if ((buf_size - i) < sizeof(TikListEntry)) break; @@ -571,12 +693,13 @@ static bool tikGetTicketEntryOffsetFromTicketList(save_ctx_t *save_ctx, u8 *buf, if (last_entry_found || success) break; } +end: return success; } -static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u64 ticket_offset, u8 titlekey_type) +static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u64 buf_size, const FsRightsId *id, u8 titlekey_type, u64 ticket_offset) { - if (!save_ctx || !buf || buf_size < SIGNED_TIK_MAX_SIZE || !id || (ticket_offset % SIGNED_TIK_MAX_SIZE) != 0) + if (!save_ctx || !buf || buf_size < SIGNED_TIK_MAX_SIZE || !id || titlekey_type >= TikTitleKeyType_Count || (ticket_offset % SIGNED_TIK_MAX_SIZE) != 0) { LOG_MSG_ERROR("Invalid parameters!"); return false; @@ -587,120 +710,142 @@ static bool tikRetrieveTicketEntryFromTicketBin(save_ctx_t *save_ctx, u8 *buf, u TikCommonBlock *tik_common_block = NULL; - Aes128CtrContext ctr_ctx = {0}; - u8 null_ctr[AES_128_KEY_SIZE] = {0}, ctr[AES_128_KEY_SIZE] = {0}, dec_tik[SIGNED_TIK_MAX_SIZE] = {0}; - bool is_volatile = false, success = false; /* Get FAT storage info for the ticket.bin stored within the opened system savefile. */ if (!save_get_fat_storage_from_file_entry_by_path(save_ctx, TIK_DB_STORAGE_PATH, &fat_storage, &ticket_bin_size)) { LOG_MSG_ERROR("Failed to locate \"%s\" in ES %s ticket system save!", TIK_DB_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); - return false; + goto end; } - /* Check ticket.bin size. */ + /* Validate ticket.bin size. */ if (ticket_bin_size < SIGNED_TIK_MIN_SIZE || (ticket_bin_size % SIGNED_TIK_MAX_SIZE) != 0 || ticket_bin_size < (ticket_offset + SIGNED_TIK_MAX_SIZE)) { LOG_MSG_ERROR("Invalid size for \"%s\" in ES %s ticket system save! (0x%lX).", TIK_DB_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type], ticket_bin_size); - return false; + goto end; } /* Read ticket data. */ if ((br = save_allocation_table_storage_read(&fat_storage, buf, ticket_offset, SIGNED_TIK_MAX_SIZE)) != SIGNED_TIK_MAX_SIZE) { LOG_MSG_ERROR("Failed to read 0x%X-byte long ticket at offset 0x%lX from \"%s\" in ES %s ticket system save!", SIGNED_TIK_MAX_SIZE, ticket_offset, TIK_DB_STORAGE_PATH, \ - g_tikTitleKeyTypeStrings[titlekey_type]); - return false; + g_tikTitleKeyTypeStrings[titlekey_type]); + goto end; } + /* Get ticket common block. */ + tik_common_block = tikGetCommonBlockFromSignedTicketBlob(buf); + /* Check if we're dealing with a volatile (encrypted) ticket. */ - if (!(tik_common_block = tikGetCommonBlock(buf)) || strncmp(tik_common_block->issuer, "Root-", 5) != 0) + is_volatile = (!tik_common_block || strncmp(tik_common_block->issuer, "Root-", 5) != 0); + if (is_volatile) { - tik_common_block = NULL; - is_volatile = true; - - /* Don't proceed if HOS version isn't at least 9.0.0. */ - if (!hosversionAtLeast(9, 0, 0)) - { - LOG_MSG_ERROR("Unable to retrieve ES key entry for volatile tickets under HOS versions below 9.0.0!"); - return false; - } - - /* Retrieve ES program memory. */ - if (!memRetrieveFullProgramMemory(&g_esMemoryLocation)) - { - LOG_MSG_ERROR("Failed to retrieve ES program memory!"); - return false; - } - - /* Retrieve the CTR key/IV from ES program memory in order to decrypt this ticket. */ - for(u64 i = 0; i < g_esMemoryLocation.data_size; i++) - { - if ((g_esMemoryLocation.data_size - i) < (sizeof(TikEsCtrKeyEntry9x) * 2)) break; - - /* Check if the key indexes are valid. idx2 should always be an odd number equal to idx + 1. */ - TikEsCtrKeyPattern9x *pattern = (TikEsCtrKeyPattern9x*)(g_esMemoryLocation.data + i); - if (pattern->idx2 != (pattern->idx1 + 1) || !(pattern->idx2 & 1)) continue; - - /* Check if the key is not null and if the CTR is. */ - TikEsCtrKeyEntry9x *key_entry = (TikEsCtrKeyEntry9x*)pattern; - if (!memcmp(key_entry->key, null_ctr, sizeof(null_ctr)) || memcmp(key_entry->ctr, null_ctr, sizeof(null_ctr)) != 0) continue; - - /* Check if we can decrypt the current ticket with this data. */ - memset(&ctr_ctx, 0, sizeof(Aes128CtrContext)); - aes128CtrInitializePartialCtr(ctr, key_entry->ctr, ticket_offset); - aes128CtrContextCreate(&ctr_ctx, key_entry->key, ctr); - aes128CtrCrypt(&ctr_ctx, dec_tik, buf, SIGNED_TIK_MAX_SIZE); - - /* Check if we successfully decrypted this ticket. */ - if ((tik_common_block = tikGetCommonBlock(dec_tik)) != NULL && !strncmp(tik_common_block->issuer, "Root-", 5)) - { - memcpy(buf, dec_tik, SIGNED_TIK_MAX_SIZE); - tik_common_block = tikGetCommonBlock(buf); - break; - } - - tik_common_block = NULL; - } - - /* Check if we were able to decrypt the ticket. */ - if (!tik_common_block) + /* Attempt to decrypt the ticket. */ + if (!tikDecryptVolatileTicket(buf, ticket_offset)) { LOG_MSG_ERROR("Unable to decrypt volatile ticket at offset 0x%lX in \"%s\" from ES %s ticket system save!", ticket_offset, TIK_DB_STORAGE_PATH, g_tikTitleKeyTypeStrings[titlekey_type]); goto end; } + + /* Get ticket common block. */ + tik_common_block = tikGetCommonBlockFromSignedTicketBlob(buf); } /* Check if the rights ID from the ticket common block matches the one we're looking for. */ - if (!(success = (memcmp(tik_common_block->rights_id.c, id->c, 0x10) == 0))) LOG_MSG_ERROR("Retrieved ticket doesn't hold a matching Rights ID!"); + if (!(success = (memcmp(tik_common_block->rights_id.c, id->c, sizeof(id->c)) == 0))) LOG_MSG_ERROR("Retrieved ticket doesn't hold a matching Rights ID!"); end: - if (is_volatile) memFreeMemoryLocation(&g_esMemoryLocation); + return success; +} + +static bool tikDecryptVolatileTicket(u8 *buf, u64 ticket_offset) +{ + if (!buf || (ticket_offset % SIGNED_TIK_MAX_SIZE) != 0) + { + LOG_MSG_ERROR("Invalid parameters!"); + return false; + } + + Aes128CtrContext ctr_ctx = {0}; + u8 null_ctr[AES_128_KEY_SIZE] = {0}, ctr[AES_128_KEY_SIZE] = {0}, dec_tik[SIGNED_TIK_MAX_SIZE] = {0}; + TikCommonBlock *tik_common_block = NULL; + bool success = false; + + /* Don't proceed if HOS version isn't at least 9.0.0. */ + if (!hosversionAtLeast(9, 0, 0)) + { + LOG_MSG_ERROR("Unable to retrieve ES key entry for volatile tickets under HOS versions below 9.0.0!"); + goto end; + } + + /* Retrieve ES program memory. */ + if (!memRetrieveFullProgramMemory(&g_esMemoryLocation)) + { + LOG_MSG_ERROR("Failed to retrieve ES program memory!"); + goto end; + } + + /* Retrieve the CTR key/IV from ES program memory in order to decrypt this ticket. */ + for(u64 i = 0; i < g_esMemoryLocation.data_size; i++) + { + if ((g_esMemoryLocation.data_size - i) < (sizeof(TikEsCtrKeyEntry9x) * 2)) break; + + /* Check if the key indexes are valid. idx2 should always be an odd number equal to idx + 1. */ + TikEsCtrKeyPattern9x *pattern = (TikEsCtrKeyPattern9x*)(g_esMemoryLocation.data + i); + if (pattern->idx2 != (pattern->idx1 + 1) || !(pattern->idx2 & 1)) continue; + + /* Check if the key is not null and if the CTR is. */ + TikEsCtrKeyEntry9x *key_entry = (TikEsCtrKeyEntry9x*)pattern; + if (!memcmp(key_entry->key, null_ctr, sizeof(null_ctr)) || memcmp(key_entry->ctr, null_ctr, sizeof(null_ctr)) != 0) continue; + + /* Check if we can decrypt the current ticket with this data. */ + memset(&ctr_ctx, 0, sizeof(Aes128CtrContext)); + aes128CtrInitializePartialCtr(ctr, key_entry->ctr, ticket_offset); + aes128CtrContextCreate(&ctr_ctx, key_entry->key, ctr); + aes128CtrCrypt(&ctr_ctx, dec_tik, buf, SIGNED_TIK_MAX_SIZE); + + /* Check if we successfully decrypted this ticket. */ + if ((tik_common_block = tikGetCommonBlockFromSignedTicketBlob(dec_tik)) != NULL && !strncmp(tik_common_block->issuer, "Root-", 5)) + { + memcpy(buf, dec_tik, SIGNED_TIK_MAX_SIZE); + success = true; + break; + } + } + + if (!success) LOG_MSG_ERROR("Unable to find ES memory key entry!"); + +end: + memFreeMemoryLocation(&g_esMemoryLocation); return success; } static bool tikGetTicketTypeAndSize(void *data, u64 data_size, u8 *out_type, u64 *out_size) { - u32 sig_type = 0; - u64 signed_ticket_size = 0; - u8 type = TikType_None; - if (!data || data_size < SIGNED_TIK_MIN_SIZE || data_size > SIGNED_TIK_MAX_SIZE || !out_type || !out_size) { LOG_MSG_ERROR("Invalid parameters!"); return false; } - if (!(signed_ticket_size = tikGetSignedTicketSize(data)) || signed_ticket_size > data_size) + u32 sig_type = 0; + u64 signed_ticket_size = 0; + u8 type = TikType_None; + bool success = false; + + /* Get signature type and signed ticket size. */ + sig_type = signatureGetTypeFromSignedBlob(data, false); + signed_ticket_size = tikGetSignedTicketBlobSize(data); + + if (!signatureIsValidType(sig_type) || signed_ticket_size < SIGNED_TIK_MIN_SIZE || signed_ticket_size > data_size) { LOG_MSG_ERROR("Input buffer doesn't hold a valid signed ticket!"); - return false; + goto end; } - sig_type = signatureGetSigType(data, false); - + /* Determine ticket type. */ switch(sig_type) { case SignatureType_Rsa4096Sha1: @@ -722,8 +867,14 @@ static bool tikGetTicketTypeAndSize(void *data, u64 data_size, u8 *out_type, u64 break; } - *out_type = type; - *out_size = signed_ticket_size; + /* Update output. */ + success = (type > TikType_None && type < TikType_Count); + if (success) + { + *out_type = type; + *out_size = signed_ticket_size; + } - return true; +end: + return success; }