From ceabad8b3f7f4b2be51bb2e78d25bacc611f51d5 Mon Sep 17 00:00:00 2001 From: d0k3 Date: Sun, 19 Oct 2025 14:38:20 +0200 Subject: [PATCH] Enable optional signature verification for NCCH & NCSD --- arm9/source/game/ncch.c | 22 ++++++++++++++++++++++ arm9/source/game/ncch.h | 1 + arm9/source/game/ncsd.c | 11 +++++++++++ arm9/source/game/ncsd.h | 1 + arm9/source/godmode.c | 24 +++++++++++++++++++----- arm9/source/lua/gm9internalfs.c | 2 +- arm9/source/system/itcm.h | 2 ++ arm9/source/utils/gameutil.c | 27 ++++++++++++++++++++------- arm9/source/utils/gameutil.h | 2 +- arm9/source/utils/scripting.c | 2 +- resources/languages/source.json | 6 +++++- 11 files changed, 84 insertions(+), 16 deletions(-) diff --git a/arm9/source/game/ncch.c b/arm9/source/game/ncch.c index ff8054a..112ed1e 100644 --- a/arm9/source/game/ncch.c +++ b/arm9/source/game/ncch.c @@ -2,6 +2,8 @@ #include "keydb.h" #include "aes.h" #include "sha.h" +#include "rsa.h" +#include "itcm.h" #define EXEFS_KEYID(name) (((strncmp(name, "banner", 8) == 0) || (strncmp(name, "icon", 8) == 0)) ? 0 : 1) @@ -28,6 +30,26 @@ u32 ValidateNcchHeader(NcchHeader* header) { return 0; } +u32 ValidateNcchSignature(NcchHeader* header, NcchExtHeader* exthdr) +{ + u8 exp[4] = { 0x00, 0x01, 0x00, 0x01 }; + u8* pubkey = ARM9_ITCM->rsaModulusCartNCSD; + + if (exthdr) { + // check extheader signature + if (!RSA_setKey2048(3, (const u32*)(const void*)ARM9_ITCM->rsaModulusAccessDesc, getle32(exp)) || + !RSA_verify2048((const u32*)(const void*)&exthdr->signature[0], (const u32*)(const void*)&exthdr->public_key[0], 0x300)) + return 1; + pubkey = exthdr->public_key; + } + + // check NCCH header signature + if (!RSA_setKey2048(3, (const u32*)(const void*)&pubkey[0], getle32(exp)) || + !RSA_verify2048((const u32*)(const void*)&header->signature[0], (const u32*)(const void*)&((u8*)header)[0x100], 0x100)) + return 1; + return 0; +} + u32 GetNcchCtr(u8* ctr, NcchHeader* ncch, u8 section) { memset(ctr, 0x00, 16); if (ncch->version == 1) { diff --git a/arm9/source/game/ncch.h b/arm9/source/game/ncch.h index 5780a13..54e69dc 100644 --- a/arm9/source/game/ncch.h +++ b/arm9/source/game/ncch.h @@ -84,6 +84,7 @@ typedef struct { } __attribute__((packed, aligned(16))) NcchHeader; u32 ValidateNcchHeader(NcchHeader* header); +u32 ValidateNcchSignature(NcchHeader* header, NcchExtHeader* exthdr); u32 SetNcchKey(NcchHeader* ncch, u16 crypto, u32 keyid); u32 SetupNcchCrypto(NcchHeader* ncch, u16 crypt_to); u32 CryptNcch(void* data, u32 offset, u32 size, NcchHeader* ncch, ExeFsHeader* exefs, u16 crypto); diff --git a/arm9/source/game/ncsd.c b/arm9/source/game/ncsd.c index 77a410e..97152e5 100644 --- a/arm9/source/game/ncsd.c +++ b/arm9/source/game/ncsd.c @@ -1,5 +1,7 @@ #include "ncsd.h" #include "ncch.h" +#include "rsa.h" +#include "itcm.h" u32 ValidateNcsdHeader(NcsdHeader* header) { static const u8 zeroes[16] = { 0 }; @@ -22,6 +24,15 @@ u32 ValidateNcsdHeader(NcsdHeader* header) { return 0; } +u32 ValidateNcsdSignature(NcsdHeader* header) { + u8 exp[4] = { 0x00, 0x01, 0x00, 0x01 }; + // this will fail for non-cart NCSDs anyways, so we don't need to check + if (!RSA_setKey2048(3, (const u32*)(const void*)&ARM9_ITCM->rsaModulusCartNCSD[0], getle32(exp)) || + !RSA_verify2048((const u32*)(const void*)&header->signature[0], (const u32*)(const void*)&((u8*)header)[0x100], 0x100)) + return 1; + return 0; +} + u64 GetNcsdTrimmedSize(NcsdHeader* header) { u32 data_units = 0; for (u32 i = 0; i < 8; i++) { diff --git a/arm9/source/game/ncsd.h b/arm9/source/game/ncsd.h index fcc3439..0a9801b 100644 --- a/arm9/source/game/ncsd.h +++ b/arm9/source/game/ncsd.h @@ -37,5 +37,6 @@ typedef struct { } PACKED_STRUCT NcsdHeader; u32 ValidateNcsdHeader(NcsdHeader* header); +u32 ValidateNcsdSignature(NcsdHeader* header); u64 GetNcsdTrimmedSize(NcsdHeader* header); u32 CryptNcsdSequential(void* data, u32 offset_data, u32 size_data, u16 crypto); diff --git a/arm9/source/godmode.c b/arm9/source/godmode.c index 39f2d01..895486e 100644 --- a/arm9/source/godmode.c +++ b/arm9/source/godmode.c @@ -1744,7 +1744,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan ShowPrompt(false, STR_PATH_TYPE_BUILD_FAILED, pathstr, type); if ((filetype & (GAME_NCCH|GAME_NCSD)) && ShowPrompt(true, "%s\n%s", pathstr, STR_FILE_FAILED_CONVERSION_VERIFY_NOW)) { - ShowPrompt(false, "%s\n%s", pathstr, (VerifyGameFile(file_path) == 0) ? STR_VERIFICATION_SUCCESS : STR_VERIFICATION_FAILED); + ShowPrompt(false, "%s\n%s", pathstr, (VerifyGameFile(file_path, false) == 0) ? STR_VERIFICATION_SUCCESS : STR_VERIFICATION_FAILED); } } } @@ -1797,7 +1797,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan ShowPrompt(false, "%s\n%s", pathstr, (ret == 0) ? STR_INSTALL_SUCCESS : STR_INSTALL_FAILED); if ((ret != 0) && (filetype & (GAME_NCCH|GAME_NCSD)) && ShowPrompt(true, "%s\n%s", pathstr, STR_FILE_FAILED_INSTALL_VERIFY_NOW)) { - ShowPrompt(false, "%s\n%s", pathstr, (VerifyGameFile(file_path) == 0) ? STR_VERIFICATION_SUCCESS : STR_VERIFICATION_FAILED); + ShowPrompt(false, "%s\n%s", pathstr, (VerifyGameFile(file_path, false) == 0) ? STR_VERIFICATION_SUCCESS : STR_VERIFICATION_FAILED); } } return 0; @@ -1841,10 +1841,24 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan return 0; } else if (user_select == verify) { // -> verify game / nand file - if ((n_marked > 1) && ShowPrompt(true, STR_TRY_TO_VERIFY_N_SELECTED_FILES, n_marked)) { + bool sig_check = false; + + // check signatures? + if (filetype & (GAME_NCSD|GAME_NCCH)) { + optionstr[0] = STR_IGNORE_SIGNATURES; + optionstr[1] = STR_VERIFY_SIGNATURES; + user_select = ShowSelectPrompt(2, optionstr, "%s", STR_USE_SIGNATURE_VERIFICATION); + if (!user_select) return 1; + sig_check = (user_select == 2); + } + + // file verification + if (n_marked > 1) { u32 n_success = 0; u32 n_other = 0; u32 n_processed = 0; + if (!ShowPrompt(true, STR_TRY_TO_VERIFY_N_SELECTED_FILES, n_marked)) // confirmation + return 1; for (u32 i = 0; i < current_dir->n_entries; i++) { const char* path = current_dir->entry[i].path; if (!current_dir->entry[i].marked) @@ -1857,7 +1871,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan } DrawDirContents(current_dir, (*cursor = i), scroll); if ((filetype & IMG_NAND) && (ValidateNandDump(path) == 0)) n_success++; - else if (VerifyGameFile(path) == 0) n_success++; + else if (VerifyGameFile(path, sig_check) == 0) n_success++; else { // on failure: show error, continue char lpathstr[UTF_BUFFER_BYTESIZE(32)]; TruncateString(lpathstr, path, 32, 8); @@ -1876,7 +1890,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan ShowString("%s\n%s", pathstr, STR_VERIFYING_FILE_PLEASE_WAIT); if (filetype & IMG_NAND) { ShowPrompt(false, "%s\n%s", pathstr, (ValidateNandDump(file_path) == 0) ? STR_NAND_VALIDATION_SUCCESS : STR_NAND_VALIDATION_FAILED); - } else ShowPrompt(false, "%s\n%s", pathstr, (VerifyGameFile(file_path) == 0) ? STR_VERIFICATION_SUCCESS : STR_VERIFICATION_FAILED); + } else ShowPrompt(false, "%s\n%s", pathstr, (VerifyGameFile(file_path, sig_check) == 0) ? STR_VERIFICATION_SUCCESS : STR_VERIFICATION_FAILED); } return 0; } diff --git a/arm9/source/lua/gm9internalfs.c b/arm9/source/lua/gm9internalfs.c index a9a56f2..56035c4 100644 --- a/arm9/source/lua/gm9internalfs.c +++ b/arm9/source/lua/gm9internalfs.c @@ -624,7 +624,7 @@ static int internalfs_verify(lua_State* L) { u64 filetype = IdentifyFileType(path); if (filetype & IMG_NAND) res = (ValidateNandDump(path) == 0); - else res = (VerifyGameFile(path) == 0); + else res = (VerifyGameFile(path, false) == 0); lua_pushboolean(L, res); return 1; diff --git a/arm9/source/system/itcm.h b/arm9/source/system/itcm.h index 518012e..c0374fb 100644 --- a/arm9/source/system/itcm.h +++ b/arm9/source/system/itcm.h @@ -162,6 +162,8 @@ typedef struct _Arm9Itcm { // Sanity checking. STATIC_ASSERT(offsetof(Arm9Itcm, otp) == 0x3800); +STATIC_ASSERT(offsetof(Arm9Itcm, rsaModulusCartNCSD) == 0x4900); +STATIC_ASSERT(offsetof(Arm9Itcm, rsaModulusAccessDesc) == 0x4A00); STATIC_ASSERT(offsetof(Arm9Itcm, twlNANDKeyY) == 0x53C8); STATIC_ASSERT(offsetof(Arm9Itcm, twlBlowfishCartKey) == 0x53E0); STATIC_ASSERT(offsetof(Arm9Itcm, ntrBlowfishCartKey) == 0x6428); diff --git a/arm9/source/utils/gameutil.c b/arm9/source/utils/gameutil.c index ded8852..abce190 100644 --- a/arm9/source/utils/gameutil.c +++ b/arm9/source/utils/gameutil.c @@ -519,7 +519,7 @@ u32 VerifyTmdContent(const char* path, u64 offset, TmdContentChunk* chunk, const return memcmp(hash, expected, 32); } -u32 VerifyNcchFile(const char* path, u32 offset, u32 size) { +u32 VerifyNcchFile(const char* path, u32 offset, u32 size, bool sig_check) { static bool cryptofix_always = false; bool cryptofix = false; NcchHeader ncch; @@ -583,6 +583,13 @@ u32 VerifyNcchFile(const char* path, u32 offset, u32 size) { return 1; } + // signature verification + if (sig_check && ValidateNcchSignature(&ncch, ncch.size_exthdr ? &exthdr : NULL) != 0) { + if (!offset) ShowPrompt(false, "%s\n%s", pathstr, STR_ERROR_SIGNATURE_CHECK_FAILED); + fvx_close(&file); + return 1; + } + // check / setup crypto if (SetupNcchCrypto(&ncch, NCCH_NOCRYPTO) != 0) { if (!offset) ShowPrompt(false, "%s\n%s", pathstr, STR_ERROR_CRYPTO_NOT_SET_UP); @@ -723,7 +730,7 @@ u32 VerifyNcchFile(const char* path, u32 offset, u32 size) { return ver_exthdr|ver_exefs|ver_romfs; } -u32 VerifyNcsdFile(const char* path) { +u32 VerifyNcsdFile(const char* path, bool sig_check) { NcsdHeader ncsd; // path string @@ -736,13 +743,19 @@ u32 VerifyNcsdFile(const char* path) { return 1; } + // signature verification + if (sig_check && ValidateNcsdSignature(&ncsd) != 0) { + ShowPrompt(false, "%s\n%s", pathstr, STR_ERROR_SIGNATURE_CHECK_FAILED); + return 1; + } + // validate NCSD contents for (u32 i = 0; i < 8; i++) { NcchPartition* partition = ncsd.partitions + i; u32 offset = partition->offset * NCSD_MEDIA_UNIT; u32 size = partition->size * NCSD_MEDIA_UNIT; if (!size) continue; - if (VerifyNcchFile(path, offset, size) != 0) { + if (VerifyNcchFile(path, offset, size, sig_check) != 0) { ShowPrompt(false, STR_PATH_CONTENT_N_SIZE_AT_OFFSET_VERIFICATION_FAILED, pathstr, i, size, offset); return 1; @@ -1032,14 +1045,14 @@ u32 VerifyTicketFile(const char* path) { return res; } -u32 VerifyGameFile(const char* path) { +u32 VerifyGameFile(const char* path, bool sig_check) { u64 filetype = IdentifyFileType(path); if (filetype & GAME_CIA) return VerifyCiaFile(path); else if (filetype & GAME_NCSD) - return VerifyNcsdFile(path); + return VerifyNcsdFile(path, sig_check); else if (filetype & GAME_NCCH) - return VerifyNcchFile(path, 0, 0); + return VerifyNcchFile(path, 0, 0, sig_check); else if (filetype & (GAME_TMD|GAME_CDNTMD|GAME_TWLTMD)) return VerifyTmdFile(path, filetype & (GAME_CDNTMD|GAME_TWLTMD)); else if (filetype & GAME_TIE) @@ -3617,7 +3630,7 @@ u32 ShowGameCheckerInfo(const char* path) { (state_tmd == 0) ? STR_STATE_INVALID : (state_tmd == 2) ? STR_STATE_LEGIT : STR_STATE_ILLEGIT, (state_verify < 0) ? STR_STATE_PENDING_PROCEED_WITH_VERIFICATION : (state_verify == 0) ? STR_STATE_PASSED : STR_STATE_FAILED) || (state_verify >= 0)) break; - state_verify = VerifyGameFile(path); + state_verify = VerifyGameFile(path, false); } if (tmd) free(tmd); diff --git a/arm9/source/utils/gameutil.h b/arm9/source/utils/gameutil.h index c001482..97e7547 100644 --- a/arm9/source/utils/gameutil.h +++ b/arm9/source/utils/gameutil.h @@ -2,7 +2,7 @@ #include "common.h" -u32 VerifyGameFile(const char* path); +u32 VerifyGameFile(const char* path, bool sig_check); u32 CheckEncryptedGameFile(const char* path); u32 CryptGameFile(const char* path, bool inplace, bool encrypt); u32 BuildCiaFromGameFile(const char* path, bool force_legit); diff --git a/arm9/source/utils/scripting.c b/arm9/source/utils/scripting.c index f7c1758..6016a34 100644 --- a/arm9/source/utils/scripting.c +++ b/arm9/source/utils/scripting.c @@ -1346,7 +1346,7 @@ bool run_cmd(cmd_id id, u32 flags, char** argv, char* err_str) { else if (id == CMD_ID_VERIFY) { u64 filetype = IdentifyFileType(argv[0]); if (filetype & IMG_NAND) ret = (ValidateNandDump(argv[0]) == 0); - else ret = (VerifyGameFile(argv[0]) == 0); + else ret = (VerifyGameFile(argv[0], false) == 0); if (err_str) snprintf(err_str, _ERR_STR_LEN, "%s", STR_VERIFICATION_FAILED); } else if (id == CMD_ID_DECRYPT) { diff --git a/resources/languages/source.json b/resources/languages/source.json index e645ae3..fcd9054 100644 --- a/resources/languages/source.json +++ b/resources/languages/source.json @@ -812,5 +812,9 @@ "SYSINFO_SYSTEM_ID0": "System ID0: %s\r\n", "SYSINFO_SYSTEM_ID1": "System ID1: %s\r\n", "SORTING_TICKETS_PLEASE_WAIT": "Sorting tickets, please wait ...", - "LUA_NOT_INCLUDED": "This build of GodMode9 was\ncompiled without Lua support." + "LUA_NOT_INCLUDED": "This build of GodMode9 was\ncompiled without Lua support.", + "ERROR_SIGNATURE_CHECK_FAILED": "Error: Signature check failed", + "USE_SIGNATURE_VERIFICATION": "Use signature verification?", + "IGNORE_SIGNATURES": "Ignore signatures", + "VERIFY_SIGNATURES": "Verify signatures" }