From 3021cfe5f034156d7d9cc2dc82576aff7915619d Mon Sep 17 00:00:00 2001 From: Pengfei Date: Fri, 30 Jul 2021 17:07:42 +0800 Subject: [PATCH] Changes to TMD and CIA builder Prepare for legit CIA. --- src/core/ncch/cia_builder.cpp | 56 +++++++++++++++++++++++-- src/core/ncch/cia_builder.h | 2 + src/core/ncch/ticket.cpp | 10 +++++ src/core/ncch/ticket.h | 3 +- src/core/ncch/title_metadata.cpp | 70 ++++++++++++++++++++++++-------- src/core/ncch/title_metadata.h | 2 + 6 files changed, 121 insertions(+), 22 deletions(-) diff --git a/src/core/ncch/cia_builder.cpp b/src/core/ncch/cia_builder.cpp index 7b558c3..f3d4743 100644 --- a/src/core/ncch/cia_builder.cpp +++ b/src/core/ncch/cia_builder.cpp @@ -2,6 +2,8 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include +#include #include #include "common/alignment.h" #include "core/importer.h" @@ -34,6 +36,10 @@ public: sha.Final(out); } + bool VerifyHash(u8* out) { + sha.Verify(out); + } + std::size_t Write(const char* data, std::size_t length) override { const std::size_t length_written = FileUtil::IOFile::Write(data, length); sha.Update(reinterpret_cast(data), length_written); @@ -70,6 +76,13 @@ bool CIABuilder::Init(CIABuildType type_, const std::string& destination, TitleM chunk.type &= ~0x01; } } + if (type == CIABuildType::Legit || type == CIABuildType::PirateLegit) { + // Check for legit TMD + if (!tmd.VerifyHashes() || !tmd.ValidateSignature()) { + LOG_ERROR(Core, "TMD is not legit"); + return false; + } + } header.header_size = sizeof(header); // Header will be written in Finalize @@ -123,14 +136,18 @@ bool CIABuilder::WriteCert(const std::string& certs_db_path) { return true; } -static bool FindLegitTicket(Ticket& out, u64 title_id, const std::string& ticket_db_path) { +static bool FindLegitTicket(Ticket& ticket, u64 title_id, const std::string& ticket_db_path) { TicketDB ticket_db(ticket_db_path); if (ticket_db.IsGood() && ticket_db.tickets.count(title_id)) { - out = ticket_db.tickets.at(title_id); + ticket = ticket_db.tickets.at(title_id); + if (!ticket.ValidateSignature()) { + LOG_ERROR(Core, "Ticket in ticket.db for {:016x} is not legit", title_id); + return false; + } return true; } - LOG_ERROR(Core, "Could not find legit ticket for {:016x}", title_id); + LOG_ERROR(Core, "Ticket for {:016x} does not exist in ticket.db", title_id); return false; } @@ -160,6 +177,25 @@ static Ticket BuildStandardTicket(u64 title_id, const std::string& ticket_db_pat return ticket; } +static Key::AESKey GetTitleKey(const Ticket& ticket) { + Key::SelectCommonKeyIndex(ticket.body.common_key_index); + if (!Key::IsNormalKeyAvailable(Key::TicketCommonKey)) { + LOG_ERROR(Core, "Ticket common key is not available"); + return {}; + } + + const auto ticket_key = Key::GetNormalKey(Key::TicketCommonKey); + Key::AESKey ctr{}; + std::memcpy(ctr.data(), &ticket.body.title_id, 8); + + CryptoPP::CTR_Mode::Decryption aes; + aes.SetKeyWithIV(ticket_key.data(), ticket_key.size(), ctr.data()); + + Key::AESKey title_key = ticket.body.title_key; + aes.ProcessData(title_key.data(), title_key.data(), title_key.size()); + return title_key; +} + bool CIABuilder::WriteTicket(const std::string& ticket_db_path, const std::string& enc_title_keys_bin_path) { @@ -173,6 +209,7 @@ bool CIABuilder::WriteTicket(const std::string& ticket_db_path, } else { ticket = BuildStandardTicket(title_id, ticket_db_path, enc_title_keys_bin_path); } + title_key = GetTitleKey(ticket); header.tik_size = ticket.GetSize(); @@ -213,7 +250,15 @@ bool CIABuilder::AddContent(u16 content_id, NCCHContainer& ncch) { auto& tmd_chunk = tmd.GetContentChunkByID(content_id); header.SetContentPresent(tmd_chunk.index); - file->GetHash(tmd_chunk.hash.data()); + + if (type == CIABuildType::Standard) { // Fix hash + file->GetHash(tmd_chunk.hash.data()); + } else { // Verify hash + if (!file->VerifyHash(tmd_chunk.hash.data())) { + LOG_ERROR(Core, "Hash dismatch for content {}", content_id); + return false; + } + } file->SetHashEnabled(false); // DLCs do not have a meta @@ -252,6 +297,9 @@ bool CIABuilder::Finalize() { } // Write TMD + if (type == CIABuildType::Standard) { + tmd.FixHashes(); + } file->Seek(tmd_offset, SEEK_SET); if (tmd.Save(*file) != ResultStatus::Success) { file.reset(); diff --git a/src/core/ncch/cia_builder.h b/src/core/ncch/cia_builder.h index 8bc85e3..87397b6 100644 --- a/src/core/ncch/cia_builder.h +++ b/src/core/ncch/cia_builder.h @@ -10,6 +10,7 @@ #include "common/file_util.h" #include "common/progress_callback.h" #include "common/swap.h" +#include "core/key/key.h" #include "core/ncch/ncch_container.h" #include "core/ncch/title_metadata.h" #include "core/quick_decryptor.h" @@ -106,6 +107,7 @@ private: Metadata meta{}; TitleMetadata tmd; + Key::AESKey title_key{}; std::size_t cert_offset{}; std::size_t ticket_offset{}; diff --git a/src/core/ncch/ticket.cpp b/src/core/ncch/ticket.cpp index 336438a..55913a3 100644 --- a/src/core/ncch/ticket.cpp +++ b/src/core/ncch/ticket.cpp @@ -4,10 +4,12 @@ #include #include +#include #include "common/alignment.h" #include "common/assert.h" #include "common/common_funcs.h" #include "common/file_util.h" +#include "common/string_util.h" #include "core/ncch/cia_common.h" #include "core/ncch/ticket.h" @@ -36,6 +38,14 @@ bool Ticket::Save(FileUtil::IOFile& file) const { return true; } +bool Ticket::ValidateSignature() const { + const auto issuer = + Common::StringFromFixedZeroTerminatedBuffer(body.issuer.data(), body.issuer.size()); + return signature.Verify(issuer, [this](CryptoPP::PK_MessageAccumulator* message) { + message->Update(reinterpret_cast(&body), sizeof(body)); + }); +} + std::size_t Ticket::GetSize() const { return signature.GetSize() + sizeof(body); } diff --git a/src/core/ncch/ticket.h b/src/core/ncch/ticket.h index 007e293..deae241 100644 --- a/src/core/ncch/ticket.h +++ b/src/core/ncch/ticket.h @@ -21,7 +21,7 @@ class Ticket { public: #pragma pack(push, 1) struct Body { - std::array issuer; + std::array issuer; std::array ecc_public_key; u8 version; u8 ca_crl_version; @@ -49,6 +49,7 @@ public: bool Load(const std::vector file_data, std::size_t offset = 0); bool Save(FileUtil::IOFile& file) const; + bool ValidateSignature() const; std::size_t GetSize() const; Signature signature; diff --git a/src/core/ncch/title_metadata.cpp b/src/core/ncch/title_metadata.cpp index ad4d8d0..552b6b5 100644 --- a/src/core/ncch/title_metadata.cpp +++ b/src/core/ncch/title_metadata.cpp @@ -62,6 +62,25 @@ ResultStatus TitleMetadata::Save(FileUtil::IOFile& file) { return ResultStatus::Error; } + // Write our TMD body, then write each of our ContentChunks + if (file.WriteBytes(&tmd_body, sizeof(TitleMetadata::Body)) != sizeof(TitleMetadata::Body)) + return ResultStatus::Error; + + for (u16 i = 0; i < tmd_body.content_count; i++) { + ContentChunk chunk = tmd_chunks[i]; + if (file.WriteBytes(&chunk, sizeof(ContentChunk)) != sizeof(ContentChunk)) + return ResultStatus::Error; + } + + return ResultStatus::Success; +} + +ResultStatus TitleMetadata::Save(const std::string& file_path) { + FileUtil::IOFile file(file_path, "wb"); + return Save(file); +} + +void TitleMetadata::FixHashes() { // Update our TMD body values and hashes tmd_body.content_count = static_cast(tmd_chunks.size()); @@ -80,26 +99,43 @@ ResultStatus TitleMetadata::Save(FileUtil::IOFile& file) { CryptoPP::SHA256 contentinfo_hash; for (std::size_t i = 0; i < tmd_body.contentinfo.size(); i++) { - chunk_hash.Update(reinterpret_cast(&tmd_body.contentinfo[i]), sizeof(ContentInfo)); + contentinfo_hash.Update(reinterpret_cast(&tmd_body.contentinfo[i]), + sizeof(ContentInfo)); } - chunk_hash.Final(tmd_body.contentinfo_hash.data()); - - // Write our TMD body, then write each of our ContentChunks - if (file.WriteBytes(&tmd_body, sizeof(TitleMetadata::Body)) != sizeof(TitleMetadata::Body)) - return ResultStatus::Error; - - for (u16 i = 0; i < tmd_body.content_count; i++) { - ContentChunk chunk = tmd_chunks[i]; - if (file.WriteBytes(&chunk, sizeof(ContentChunk)) != sizeof(ContentChunk)) - return ResultStatus::Error; - } - - return ResultStatus::Success; + contentinfo_hash.Final(tmd_body.contentinfo_hash.data()); } -ResultStatus TitleMetadata::Save(const std::string& file_path) { - FileUtil::IOFile file(file_path, "wb"); - return Save(file); +bool TitleMetadata::VerifyHashes() const { + // This can probably be simplified to just checking contentinfo 0 (as above), but do the full + // thing for completeness. + // TODO: Is this what index and command_count actually mean? + CryptoPP::SHA256 contentinfo_hash; + for (std::size_t i = 0; i < tmd_body.contentinfo.size(); i++) { + contentinfo_hash.Update(reinterpret_cast(&tmd_body.contentinfo[i]), + sizeof(ContentInfo)); + const std::size_t offset = tmd_body.contentinfo[i].index; + const std::size_t count = tmd_body.contentinfo[i].command_count; + if (count == 0) { + continue; + } + if (offset + count > tmd_body.content_count) { // sanity check + LOG_INFO(Core, "Index / count out of bound for content info record {}", i); + return false; + } + CryptoPP::SHA256 chunk_hash; + for (std::size_t j = offset; j < offset + count; ++j) { + chunk_hash.Update(reinterpret_cast(&tmd_chunks[j]), sizeof(ContentChunk)); + } + if (!chunk_hash.Verify(tmd_body.contentinfo[i].hash.data())) { + LOG_ERROR(Core, "Chunk hash dismatch for content info record {}", i); + return false; + } + } + if (!contentinfo_hash.Verify(tmd_body.contentinfo_hash.data())) { + LOG_ERROR(Core, "Content info hash dismatch"); + return false; + } + return true; } bool TitleMetadata::ValidateSignature() const { diff --git a/src/core/ncch/title_metadata.h b/src/core/ncch/title_metadata.h index 3bc9a9a..98a9314 100644 --- a/src/core/ncch/title_metadata.h +++ b/src/core/ncch/title_metadata.h @@ -83,6 +83,8 @@ public: ResultStatus Save(FileUtil::IOFile& file); ResultStatus Save(const std::string& file_path); + void FixHashes(); + bool VerifyHashes() const; bool ValidateSignature() const; std::size_t GetSize() const;