Changes to TMD and CIA builder

Prepare for legit CIA.
This commit is contained in:
Pengfei
2021-07-30 17:07:42 +08:00
parent d14ea03271
commit 3021cfe5f0
6 changed files with 121 additions and 22 deletions
+52 -4
View File
@@ -2,6 +2,8 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cryptopp/sha.h>
#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<const CryptoPP::byte*>(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<CryptoPP::AES>::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();
+2
View File
@@ -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{};
+10
View File
@@ -4,10 +4,12 @@
#include <cstring>
#include <string_view>
#include <cryptopp/rsa.h>
#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<const u8*>(&body), sizeof(body));
});
}
std::size_t Ticket::GetSize() const {
return signature.GetSize() + sizeof(body);
}
+2 -1
View File
@@ -21,7 +21,7 @@ class Ticket {
public:
#pragma pack(push, 1)
struct Body {
std::array<u8, 0x40> issuer;
std::array<char, 0x40> issuer;
std::array<u8, 0x3C> ecc_public_key;
u8 version;
u8 ca_crl_version;
@@ -49,6 +49,7 @@ public:
bool Load(const std::vector<u8> file_data, std::size_t offset = 0);
bool Save(FileUtil::IOFile& file) const;
bool ValidateSignature() const;
std::size_t GetSize() const;
Signature signature;
+53 -17
View File
@@ -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<u16>(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<u8*>(&tmd_body.contentinfo[i]), sizeof(ContentInfo));
contentinfo_hash.Update(reinterpret_cast<u8*>(&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<const u8*>(&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<const u8*>(&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 {
+2
View File
@@ -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;