[nstool] Add NCA sig[1] validation, better code structure.

This commit is contained in:
jakcron 2018-05-12 00:26:19 +08:00
parent 3d57420c70
commit a6c30a8f0f
3 changed files with 321 additions and 161 deletions

View file

@ -0,0 +1,17 @@
#pragma once
#include <fnd/IFile.h>
class CopiedIFile : public fnd::IFile
{
public:
inline CopiedIFile(fnd::IFile* file) : mFile(file) {}
inline size_t size() { return mFile->size(); }
inline void seek(size_t offset) { mFile->seek(offset); }
inline void read(byte_t* out, size_t len) { mFile->read(out, len); }
inline void read(byte_t* out, size_t offset, size_t len) { mFile->read(out, offset, len); }
inline void write(const byte_t* out, size_t len) { mFile->write(out, len); }
inline void write(const byte_t* out, size_t offset, size_t len) { mFile->write(out, offset, len); }
private:
fnd::IFile* mFile;
};

View file

@ -1,11 +1,14 @@
#include <sstream>
#include <fnd/SimpleTextOutput.h> #include <fnd/SimpleTextOutput.h>
#include <nx/NcaUtils.h> #include <nx/NcaUtils.h>
#include <nx/AesKeygen.h> #include <nx/AesKeygen.h>
#include <nx/NpdmBinary.h>
#include "NcaProcess.h" #include "NcaProcess.h"
#include "PfsProcess.h" #include "PfsProcess.h"
#include "RomfsProcess.h" #include "RomfsProcess.h"
#include "OffsetAdjustedIFile.h" #include "OffsetAdjustedIFile.h"
#include "AesCtrWrappedIFile.h" #include "AesCtrWrappedIFile.h"
#include "CopiedIFile.h"
std::string kFormatVersionStr[] std::string kFormatVersionStr[]
{ {
@ -59,6 +62,240 @@ std::string kKaekIndexStr[]
"System" "System"
}; };
void NcaProcess::generateNcaBodyEncryptionKeys()
{
// create zeros key
crypto::aes::sAes128Key zero_aesctr_key;
memset(zero_aesctr_key.key, 0, sizeof(zero_aesctr_key));
crypto::aes::sAesXts128Key zero_aesxts_key;
memset(zero_aesxts_key.key, 0, sizeof(zero_aesxts_key));
// get key data from header
byte_t masterkey_rev = nx::NcaUtils::getMasterKeyRevisionFromKeyGeneration(mHdr.getKeyGeneration());
byte_t keak_index = mHdr.getKaekIndex();
// set flag to indicate that the keys are not available
mBodyKeys.aes_ctr.isSet = false;
mBodyKeys.aes_xts.isSet = false;
// if this has a rights id, the key needs to be sourced from a ticket
if (mHdr.hasRightsId() == true)
{
// if the titlekey_kek is available
if (mKeyset->ticket.titlekey_kek[masterkey_rev] != zero_aesctr_key)
{
crypto::aes::sAesIvCtr iv;
iv.set(mHdr.getRightsId());
// the title key is provided (sourced from ticket)
if (mKeyset->nca.manual_title_key_aesctr != zero_aesctr_key)
{
crypto::aes::AesCbcDecrypt(mKeyset->nca.manual_title_key_aesctr.key, 16, mKeyset->ticket.titlekey_kek[masterkey_rev].key, iv.iv, mBodyKeys.aes_ctr.var.key);
mBodyKeys.aes_ctr.isSet = true;
}
if (mKeyset->nca.manual_title_key_aesxts != zero_aesxts_key)
{
crypto::aes::AesCbcDecrypt(mKeyset->nca.manual_title_key_aesxts.key[0], 16, mKeyset->ticket.titlekey_kek[masterkey_rev].key, iv.iv, mBodyKeys.aes_xts.var.key[0]);
crypto::aes::AesCbcDecrypt(mKeyset->nca.manual_title_key_aesxts.key[1], 16, mKeyset->ticket.titlekey_kek[masterkey_rev].key, iv.iv, mBodyKeys.aes_xts.var.key[1]);
mBodyKeys.aes_xts.isSet = true;
}
}
}
// otherwise decrypt key area
else
{
// if the titlekey_kek is available
if (mKeyset->nca.key_area_key[keak_index][masterkey_rev] != zero_aesctr_key)
{
nx::AesKeygen::generateKey(mBodyKeys.aes_ctr.var.key, mHdr.getEncAesKeys()[nx::nca::KEY_AESCTR].key, mKeyset->nca.key_area_key[keak_index][masterkey_rev].key);
mBodyKeys.aes_ctr.isSet = true;
nx::AesKeygen::generateKey(mBodyKeys.aes_xts.var.key[0], mHdr.getEncAesKeys()[nx::nca::KEY_AESXTS_0].key, mKeyset->nca.key_area_key[keak_index][masterkey_rev].key);
nx::AesKeygen::generateKey(mBodyKeys.aes_xts.var.key[1], mHdr.getEncAesKeys()[nx::nca::KEY_AESXTS_1].key, mKeyset->nca.key_area_key[keak_index][masterkey_rev].key);
mBodyKeys.aes_xts.isSet = true;
}
}
// if the keys weren't generated, check if the keys were supplied by the user
if (mBodyKeys.aes_ctr.isSet == false && mKeyset->nca.manual_body_key_aesctr != zero_aesctr_key)
{
mBodyKeys.aes_ctr = mKeyset->nca.manual_body_key_aesctr;
}
if (mBodyKeys.aes_xts.isSet == false && mKeyset->nca.manual_body_key_aesxts != zero_aesxts_key)
{
mBodyKeys.aes_xts = mKeyset->nca.manual_body_key_aesxts;
}
}
void NcaProcess::generatePartitionConfiguration()
{
std::stringstream error;
for (size_t i = 0; i < mHdr.getPartitions().getSize(); i++)
{
// get reference to relevant structures
const nx::NcaHeader::sPartition& partition = mHdr.getPartitions()[i];
nx::sNcaFsHeader& fs_header = mHdrBlock.fs_header[partition.index];
// validate header hash
crypto::sha::sSha256Hash calc_hash;
crypto::sha::Sha256((const byte_t*)&mHdrBlock.fs_header[partition.index], sizeof(nx::sNcaFsHeader), calc_hash.bytes);
if (calc_hash.compare(partition.hash) == false)
{
error.clear();
error << "NCA FS Header [" << partition.index << "] Hash: FAIL \n";
throw fnd::Exception(kModuleName, error.str());
}
// setup AES-CTR
crypto::aes::sAesIvCtr ctr;
nx::NcaUtils::getNcaPartitionAesCtr(&fs_header, ctr.iv);
// save partition config
mPartitions[partition.index].reader = nullptr;
mPartitions[partition.index].offset = partition.offset;
mPartitions[partition.index].size = partition.size;
mPartitions[partition.index].format_type = (nx::nca::FormatType)fs_header.format_type;
mPartitions[partition.index].hash_type = (nx::nca::HashType)fs_header.hash_type;
memcpy(mPartitions[partition.index].hash_superblock, fs_header.hash_superblock, nx::nca::kFsHeaderHashSuperblockLen);
// filter out unrecognised format types
switch (mPartitions[partition.index].format_type)
{
case (nx::nca::FORMAT_PFS0):
case (nx::nca::FORMAT_ROMFS):
break;
default:
continue;
}
// filter out unrecognised hash types
switch (mPartitions[partition.index].hash_type)
{
case (nx::nca::HASH_NONE):
case (nx::nca::HASH_HIERARCHICAL_SHA256):
case (nx::nca::HASH_HIERARCHICAL_INTERGRITY):
break;
default:
continue;
}
// create reader
switch(fs_header.encryption_type)
{
case (nx::nca::CRYPT_AESXTS):
case (nx::nca::CRYPT_AESCTREX):
mPartitions[partition.index].reader = nullptr;
break;
case (nx::nca::CRYPT_AESCTR):
mPartitions[partition.index].reader = mBodyKeys.aes_ctr.isSet? new AesCtrWrappedIFile(mReader, mBodyKeys.aes_ctr.var, ctr) : nullptr;
break;
case (nx::nca::CRYPT_NONE):
mPartitions[partition.index].reader = new CopiedIFile(mReader);
break;
default:
error.clear();
error << "NCA FS Header [" << partition.index << "] EncryptionType(" << fs_header.encryption_type << "): UNKNOWN \n";
throw fnd::Exception(kModuleName, error.str());
}
// determine the data offset
if (mPartitions[partition.index].hash_type == nx::nca::HASH_HIERARCHICAL_SHA256)
{
mPartitions[partition.index].data_offset = mPartitions[partition.index].hierarchicalsha256_header.hash_target.offset.get();
}
else if (mPartitions[partition.index].hash_type == nx::nca::HASH_HIERARCHICAL_INTERGRITY)
{
for (size_t j = 0; j < nx::ivfc::kMaxIvfcLevel; j++)
{
if (mPartitions[partition.index].ivfc_header.level_header[nx::ivfc::kMaxIvfcLevel-1-j].logical_offset.get() != 0)
{
mPartitions[partition.index].data_offset = mPartitions[partition.index].ivfc_header.level_header[nx::ivfc::kMaxIvfcLevel-1-j].logical_offset.get();
break;
}
}
}
else if (mPartitions[partition.index].hash_type == nx::nca::HASH_NONE)
{
mPartitions[partition.index].data_offset = 0;
}
}
}
void NcaProcess::validatePartitionHash()
{
}
void NcaProcess::validateNcaSignatures()
{
// validate signature[0]
if (crypto::rsa::pss::rsaVerify(mKeyset->nca.header_sign_key, crypto::sha::HASH_SHA256, mHdrHash.bytes, mHdrBlock.signature_main) != 0)
{
// this is minimal even though it's a warning because it's a validation method
if (mCliOutputType >= OUTPUT_MINIMAL)
printf("[WARNING] NCA Header Main Signature: FAIL \n");
}
// validate signature[1]
if (mHdr.getContentType() == nx::nca::TYPE_PROGRAM)
{
if (mPartitions[0].format_type == nx::nca::FORMAT_PFS0)
{
if (mPartitions[0].reader != nullptr)
{
PfsProcess exefs;
exefs.setInputFile(mPartitions[0].reader);
exefs.setInputFileOffset(mPartitions[0].offset + mPartitions[0].data_offset);
exefs.setCliOutputMode(OUTPUT_MINIMAL);
exefs.process();
// open main.npdm
if (exefs.getPfsHeader().getFileList().hasElement(kNpdmExefsPath) == true)
{
const nx::PfsHeader::sFile& npdmFile = exefs.getPfsHeader().getFileList()[exefs.getPfsHeader().getFileList().getIndexOf(kNpdmExefsPath)];
fnd::MemoryBlob scratch;
scratch.alloc(npdmFile.size);
mPartitions[0].reader->read(scratch.getBytes(), mPartitions[0].offset + mPartitions[0].data_offset + npdmFile.offset, npdmFile.size);
nx::NpdmBinary npdmBinary;
npdmBinary.importBinary(scratch.getBytes(), scratch.getSize());
if (crypto::rsa::pss::rsaVerify(npdmBinary.getAcid().getNcaHeader2RsaKey(), crypto::sha::HASH_SHA256, mHdrHash.bytes, mHdrBlock.signature_acid) != 0)
{
// this is minimal even though it's a warning because it's a validation method
if (mCliOutputType >= OUTPUT_MINIMAL)
printf("[WARNING] NCA Header ACID Signature: FAIL \n");
}
}
else
{
// this is minimal even though it's a warning because it's a validation method
if (mCliOutputType >= OUTPUT_MINIMAL)
printf("[WARNING] NCA Header ACID Signature: FAIL (\"%s\" not present in ExeFs)\n", kNpdmExefsPath.c_str());
}
}
else
{
// this is minimal even though it's a warning because it's a validation method
if (mCliOutputType >= OUTPUT_MINIMAL)
printf("[WARNING] NCA Header ACID Signature: FAIL (ExeFs unreadable)\n");
}
}
else
{
// this is minimal even though it's a warning because it's a validation method
if (mCliOutputType >= OUTPUT_MINIMAL)
printf("[WARNING] NCA Header ACID Signature: FAIL (No ExeFs partition)\n");
}
}
}
void NcaProcess::displayHeader() void NcaProcess::displayHeader()
{ {
crypto::aes::sAes128Key zero_key; crypto::aes::sAes128Key zero_key;
@ -174,144 +411,46 @@ void NcaProcess::displayHeader()
} }
} }
void NcaProcess::generateNcaBodyEncryptionKeys()
{
// create zeros key
crypto::aes::sAes128Key zero_aesctr_key;
memset(zero_aesctr_key.key, 0, sizeof(zero_aesctr_key));
crypto::aes::sAesXts128Key zero_aesxts_key;
memset(zero_aesxts_key.key, 0, sizeof(zero_aesxts_key));
// get key data from header
byte_t masterkey_rev = nx::NcaUtils::getMasterKeyRevisionFromKeyGeneration(mHdr.getKeyGeneration());
byte_t keak_index = mHdr.getKaekIndex();
// set flag to indicate that the keys are not available
mBodyKeys.aes_ctr.isSet = false;
mBodyKeys.aes_xts.isSet = false;
// if this has a rights id, the key needs to be sourced from a ticket
if (mHdr.hasRightsId() == true)
{
// if the titlekey_kek is available
if (mKeyset->ticket.titlekey_kek[masterkey_rev] != zero_aesctr_key)
{
// the title key is provided (sourced from ticket)
if (mKeyset->nca.manual_title_key_aesctr != zero_aesctr_key)
{
nx::AesKeygen::generateKey(mBodyKeys.aes_ctr.var.key, mKeyset->nca.manual_title_key_aesctr.key, mKeyset->ticket.titlekey_kek[masterkey_rev].key);
mBodyKeys.aes_ctr.isSet = true;
}
if (mKeyset->nca.manual_title_key_aesxts != zero_aesxts_key)
{
nx::AesKeygen::generateKey(mBodyKeys.aes_xts.var.key[0], mKeyset->nca.manual_title_key_aesxts.key[0], mKeyset->ticket.titlekey_kek[masterkey_rev].key);
nx::AesKeygen::generateKey(mBodyKeys.aes_xts.var.key[1], mKeyset->nca.manual_title_key_aesxts.key[1], mKeyset->ticket.titlekey_kek[masterkey_rev].key);
mBodyKeys.aes_xts.isSet = true;
}
}
}
// otherwise decrypt key area
else
{
// if the titlekey_kek is available
if (mKeyset->nca.key_area_key[keak_index][masterkey_rev] != zero_aesctr_key)
{
nx::AesKeygen::generateKey(mBodyKeys.aes_ctr.var.key, mHdr.getEncAesKeys()[nx::nca::KEY_AESCTR].key, mKeyset->nca.key_area_key[keak_index][masterkey_rev].key);
mBodyKeys.aes_ctr.isSet = true;
nx::AesKeygen::generateKey(mBodyKeys.aes_xts.var.key[0], mHdr.getEncAesKeys()[nx::nca::KEY_AESXTS_0].key, mKeyset->nca.key_area_key[keak_index][masterkey_rev].key);
nx::AesKeygen::generateKey(mBodyKeys.aes_xts.var.key[1], mHdr.getEncAesKeys()[nx::nca::KEY_AESXTS_1].key, mKeyset->nca.key_area_key[keak_index][masterkey_rev].key);
mBodyKeys.aes_xts.isSet = true;
}
}
// if the keys weren't generated, check if the keys were supplied by the user
if (mBodyKeys.aes_ctr.isSet == false && mKeyset->nca.manual_body_key_aesctr != zero_aesctr_key)
{
mBodyKeys.aes_ctr = mKeyset->nca.manual_body_key_aesctr;
}
if (mBodyKeys.aes_xts.isSet == false && mKeyset->nca.manual_body_key_aesxts != zero_aesxts_key)
{
mBodyKeys.aes_xts = mKeyset->nca.manual_body_key_aesxts;
}
}
void NcaProcess::processPartitions() void NcaProcess::processPartitions()
{ {
for (size_t i = 0; i < mHdr.getPartitions().getSize(); i++) for (size_t i = 0; i < mHdr.getPartitions().getSize(); i++)
{ {
const nx::NcaHeader::sPartition& partition = mHdr.getPartitions()[i]; size_t index = mHdr.getPartitions()[i].index;
nx::sNcaFsHeader& fs_header = mHdrBlock.fs_header[partition.index]; struct sPartitionInfo& partition = mPartitions[index];
crypto::aes::sAesIvCtr ctr;
nx::NcaUtils::getNcaPartitionAesCtr(&fs_header, ctr.iv);
// create reader
fnd::IFile* partitionReader = nullptr;
AesCtrWrappedIFile aesCtrFile = AesCtrWrappedIFile(mReader, mBodyKeys.aes_ctr.var, ctr);
switch(fs_header.encryption_type)
{
case (nx::nca::CRYPT_AESXTS):
case (nx::nca::CRYPT_AESCTREX):
partitionReader = nullptr;
break;
case (nx::nca::CRYPT_AESCTR):
partitionReader = mBodyKeys.aes_ctr.isSet? &aesCtrFile : nullptr;
break;
case (nx::nca::CRYPT_NONE):
partitionReader = mReader;
break;
}
// if the reader is null, skip // if the reader is null, skip
if (partitionReader == nullptr) if (partition.reader == nullptr)
{ {
printf("[WARNING] NCA Partition %d not readable\n", partition.index); printf("[WARNING] NCA Partition %d not readable\n", index);
continue; continue;
} }
size_t data_offset = 0; if (partition.format_type == nx::nca::FORMAT_PFS0)
switch (fs_header.hash_type)
{
case (nx::nca::HASH_HIERARCHICAL_SHA256):
data_offset = fs_header.hierarchicalsha256_header.hash_target.offset.get();
break;
case (nx::nca::HASH_HIERARCHICAL_INTERGRITY):
data_offset = fs_header.ivfc_header.level_header[5].logical_offset.get();
break;
case (nx::nca::HASH_NONE):
data_offset = 0;
break;
default:
throw fnd::Exception(kModuleName, "Unknown hash type");
}
if (fs_header.format_type == nx::nca::FORMAT_PFS0)
{ {
PfsProcess pfs; PfsProcess pfs;
pfs.setInputFile(partitionReader); pfs.setInputFile(partition.reader);
pfs.setInputFileOffset(partition.offset + data_offset); pfs.setInputFileOffset(partition.offset + partition.data_offset);
pfs.setCliOutputMode(mCliOutputType); pfs.setCliOutputMode(mCliOutputType);
pfs.setListFs(mListFs); pfs.setListFs(mListFs);
if (mPartitionPath[partition.index].doExtract) if (mPartitionPath[index].doExtract)
pfs.setExtractPath(mPartitionPath[partition.index].path); pfs.setExtractPath(mPartitionPath[index].path);
//printf("pfs.process(%lx)\n",partition.data_offset);
pfs.process(); pfs.process();
//printf("pfs.process() end\n");
} }
else if (fs_header.format_type == nx::nca::FORMAT_ROMFS) else if (partition.format_type == nx::nca::FORMAT_ROMFS)
{ {
RomfsProcess romfs; RomfsProcess romfs;
romfs.setInputFile(partitionReader); romfs.setInputFile(partition.reader);
romfs.setInputFileOffset(partition.offset + data_offset); romfs.setInputFileOffset(partition.offset + partition.data_offset);
romfs.setCliOutputMode(mCliOutputType); romfs.setCliOutputMode(mCliOutputType);
romfs.setListFs(mListFs); romfs.setListFs(mListFs);
if (mPartitionPath[partition.index].doExtract) if (mPartitionPath[index].doExtract)
romfs.setExtractPath(mPartitionPath[partition.index].path); romfs.setExtractPath(mPartitionPath[index].path);
//printf("romfs.process(%lx)\n", partition.data_offset);
romfs.process(); romfs.process();
} //printf("romfs.process() end\n");
else
{
throw fnd::Exception(kModuleName, "Unknown format type");
} }
} }
} }
@ -324,15 +463,22 @@ NcaProcess::NcaProcess() :
mVerify(false), mVerify(false),
mListFs(false) mListFs(false)
{ {
mPartitionPath[0].doExtract = false; for (size_t i = 0; i < nx::nca::kPartitionNum; i++)
mPartitionPath[1].doExtract = false; {
mPartitionPath[2].doExtract = false; mPartitionPath[i].doExtract = false;
mPartitionPath[3].doExtract = false; mPartitions[i].reader = nullptr;
}
} }
NcaProcess::~NcaProcess() NcaProcess::~NcaProcess()
{ {
for (size_t i = 0; i < nx::nca::kPartitionNum; i++)
{
if (mPartitions[i].reader != nullptr)
{
delete mPartitions[i].reader;
}
}
} }
void NcaProcess::process() void NcaProcess::process()
@ -353,42 +499,25 @@ void NcaProcess::process()
// generate header hash // generate header hash
crypto::sha::Sha256((byte_t*)&mHdrBlock.header, sizeof(nx::sNcaHeader), mHdrHash.bytes); crypto::sha::Sha256((byte_t*)&mHdrBlock.header, sizeof(nx::sNcaHeader), mHdrHash.bytes);
// validate signature[0]
if (mVerify)
{
if (crypto::rsa::pss::rsaVerify(mKeyset->nca.header_sign_key, crypto::sha::HASH_SHA256, mHdrHash.bytes, mHdrBlock.signature_main) != 0)
{
// this is minimal even though it's a warning because it's a validation method
if (mCliOutputType >= OUTPUT_MINIMAL)
printf("[WARNING] NCA Header Main Signature: FAIL \n");
}
}
// proccess main header // proccess main header
mHdr.importBinary((byte_t*)&mHdrBlock.header, sizeof(nx::sNcaHeader)); mHdr.importBinary((byte_t*)&mHdrBlock.header, sizeof(nx::sNcaHeader));
// validate fs headers
if (mVerify)
{
crypto::sha::sSha256Hash calc_hash;
for (size_t i = 0; i < mHdr.getPartitions().getSize(); i++)
{
const nx::NcaHeader::sPartition& partition = mHdr.getPartitions()[i];
crypto::sha::Sha256((const byte_t*)&mHdrBlock.fs_header[partition.index], sizeof(nx::sNcaFsHeader), calc_hash.bytes);
if (calc_hash.compare(partition.hash) == false)
{
// this is minimal even though it's a warning because it's a validation method
if (mCliOutputType >= OUTPUT_MINIMAL)
printf("[WARNING] NCA FsHeader[%d] Hash: FAIL \n", partition.index);
}
}
}
// determine keys // determine keys
generateNcaBodyEncryptionKeys();
// import/generate fs header data
generatePartitionConfiguration();
// validate signatures
if (mVerify)
validateNcaSignatures();
// display header
if (mCliOutputType >= OUTPUT_NORMAL)
displayHeader();
// process partition
processPartitions();
/* /*
NCA is a file container NCA is a file container
@ -413,12 +542,9 @@ void NcaProcess::process()
// decrypt key area // decrypt key area
generateNcaBodyEncryptionKeys();
if (mCliOutputType >= OUTPUT_NORMAL)
displayHeader();
processPartitions();
} }
void NcaProcess::setInputFile(fnd::IFile* reader) void NcaProcess::setInputFile(fnd::IFile* reader)

View file

@ -30,6 +30,7 @@ public:
private: private:
const std::string kModuleName = "NcaProcess"; const std::string kModuleName = "NcaProcess";
const std::string kNpdmExefsPath = "main.npdm";
// user options // user options
fnd::IFile* mReader; fnd::IFile* mReader;
@ -57,11 +58,27 @@ private:
sOptional<crypto::aes::sAes128Key> aes_ctr; sOptional<crypto::aes::sAes128Key> aes_ctr;
sOptional<crypto::aes::sAesXts128Key> aes_xts; sOptional<crypto::aes::sAesXts128Key> aes_xts;
} mBodyKeys; } mBodyKeys;
struct sPartitionInfo
{
fnd::IFile* reader;
size_t offset;
size_t data_offset;
size_t size;
nx::nca::FormatType format_type;
nx::nca::HashType hash_type;
void displayHeader(); union {
byte_t hash_superblock[nx::nca::kFsHeaderHashSuperblockLen];
nx::sHierarchicalSha256Header hierarchicalsha256_header;
nx::sIvfcHeader ivfc_header;
};
} mPartitions[nx::nca::kPartitionNum];
void generateNcaBodyEncryptionKeys(); void generateNcaBodyEncryptionKeys();
void generatePartitionConfiguration();
void validatePartitionHash();
void validateNcaSignatures();
void displayHeader();
void processPartitions(); void processPartitions();
}; };