key_manager: Make ticket reading more robust.
This commit is contained in:
parent
2f569f6384
commit
64f3ad2654
4 changed files with 140 additions and 96 deletions
|
@ -35,7 +35,6 @@ namespace Core::Crypto {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr u64 CURRENT_CRYPTO_REVISION = 0x5;
|
constexpr u64 CURRENT_CRYPTO_REVISION = 0x5;
|
||||||
constexpr u64 FULL_TICKET_SIZE = 0x400;
|
|
||||||
|
|
||||||
using Common::AsArray;
|
using Common::AsArray;
|
||||||
|
|
||||||
|
@ -214,34 +213,51 @@ Ticket Ticket::SynthesizeCommon(Key128 title_key, const std::array<u8, 16>& righ
|
||||||
return Ticket{out};
|
return Ticket{out};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Ticket::Read(Ticket& ticket_out, const FileSys::VirtualFile& file) {
|
Ticket Ticket::Read(const FileSys::VirtualFile& file) {
|
||||||
SignatureType sig_type;
|
// Attempt to read up to the largest ticket size, and make sure we read at least a signature
|
||||||
if (file->Read(reinterpret_cast<u8*>(&sig_type), sizeof(sig_type), 0) < sizeof(sig_type)) {
|
// type.
|
||||||
return false;
|
std::array<u8, sizeof(RSA4096Ticket)> raw_data{};
|
||||||
|
auto read_size = file->Read(raw_data.data(), raw_data.size(), 0);
|
||||||
|
if (read_size < sizeof(SignatureType)) {
|
||||||
|
LOG_WARNING(Crypto, "Attempted to read ticket file with invalid size {}.", read_size);
|
||||||
|
return Ticket{std::monostate()};
|
||||||
}
|
}
|
||||||
|
return Read(std::span{raw_data});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ticket Ticket::Read(std::span<const u8> raw_data) {
|
||||||
|
// Some tools read only 0x180 bytes of ticket data instead of 0x2C0, so
|
||||||
|
// just make sure we have at least the bare minimum of data to work with.
|
||||||
|
SignatureType sig_type;
|
||||||
|
if (raw_data.size() < sizeof(SignatureType)) {
|
||||||
|
LOG_WARNING(Crypto, "Attempted to parse ticket buffer with invalid size {}.",
|
||||||
|
raw_data.size());
|
||||||
|
return Ticket{std::monostate()};
|
||||||
|
}
|
||||||
|
std::memcpy(&sig_type, raw_data.data(), sizeof(sig_type));
|
||||||
|
|
||||||
switch (sig_type) {
|
switch (sig_type) {
|
||||||
case SignatureType::RSA_4096_SHA1:
|
case SignatureType::RSA_4096_SHA1:
|
||||||
case SignatureType::RSA_4096_SHA256: {
|
case SignatureType::RSA_4096_SHA256: {
|
||||||
ticket_out.data.emplace<RSA4096Ticket>();
|
RSA4096Ticket ticket{};
|
||||||
file->Read(reinterpret_cast<u8*>(&ticket_out.data), sizeof(RSA4096Ticket), 0);
|
std::memcpy(&ticket, raw_data.data(), sizeof(ticket));
|
||||||
return true;
|
return Ticket{ticket};
|
||||||
}
|
}
|
||||||
case SignatureType::RSA_2048_SHA1:
|
case SignatureType::RSA_2048_SHA1:
|
||||||
case SignatureType::RSA_2048_SHA256: {
|
case SignatureType::RSA_2048_SHA256: {
|
||||||
ticket_out.data.emplace<RSA2048Ticket>();
|
RSA2048Ticket ticket{};
|
||||||
file->Read(reinterpret_cast<u8*>(&ticket_out.data), sizeof(RSA2048Ticket), 0);
|
std::memcpy(&ticket, raw_data.data(), sizeof(ticket));
|
||||||
return true;
|
return Ticket{ticket};
|
||||||
}
|
}
|
||||||
case SignatureType::ECDSA_SHA1:
|
case SignatureType::ECDSA_SHA1:
|
||||||
case SignatureType::ECDSA_SHA256: {
|
case SignatureType::ECDSA_SHA256: {
|
||||||
ticket_out.data.emplace<ECDSATicket>();
|
ECDSATicket ticket{};
|
||||||
file->Read(reinterpret_cast<u8*>(&ticket_out.data), sizeof(ECDSATicket), 0);
|
std::memcpy(&ticket, raw_data.data(), sizeof(ticket));
|
||||||
return true;
|
return Ticket{ticket};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
ticket_out.data.emplace<std::monostate>();
|
LOG_WARNING(Crypto, "Attempted to parse ticket buffer with invalid type {}.", sig_type);
|
||||||
return false;
|
return Ticket{std::monostate()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,10 +498,12 @@ std::vector<Ticket> GetTicketblob(const Common::FS::IOFile& ticket_save) {
|
||||||
for (std::size_t offset = 0; offset + 0x4 < buffer.size(); ++offset) {
|
for (std::size_t offset = 0; offset + 0x4 < buffer.size(); ++offset) {
|
||||||
if (buffer[offset] == 0x4 && buffer[offset + 1] == 0x0 && buffer[offset + 2] == 0x1 &&
|
if (buffer[offset] == 0x4 && buffer[offset + 1] == 0x0 && buffer[offset + 2] == 0x1 &&
|
||||||
buffer[offset + 3] == 0x0) {
|
buffer[offset + 3] == 0x0) {
|
||||||
out.emplace_back();
|
// NOTE: Assumes ticket blob will only contain RSA-2048 tickets.
|
||||||
auto& next = out.back();
|
auto ticket = Ticket::Read(std::span{buffer.data() + offset, sizeof(RSA2048Ticket)});
|
||||||
std::memcpy(&next, buffer.data() + offset, sizeof(Ticket));
|
offset += sizeof(RSA2048Ticket);
|
||||||
offset += FULL_TICKET_SIZE;
|
if (ticket.IsValid()) {
|
||||||
|
out.push_back(ticket);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -544,71 +562,66 @@ std::optional<std::pair<Key128, Key128>> ParseTicket(const Ticket& ticket,
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dirty hack, figure out why ticket.data variant is invalid
|
const auto issuer = ticket.GetData().issuer;
|
||||||
try {
|
if (IsAllZeroArray(issuer)) {
|
||||||
const auto issuer = ticket.GetData().issuer;
|
|
||||||
if (IsAllZeroArray(issuer)) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
if (issuer[0] != 'R' || issuer[1] != 'o' || issuer[2] != 'o' || issuer[3] != 't') {
|
|
||||||
LOG_INFO(Crypto, "Attempting to parse ticket with non-standard certificate authority.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Key128 rights_id = ticket.GetData().rights_id;
|
|
||||||
|
|
||||||
if (rights_id == Key128{}) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ticket.GetData().type == TitleKeyType::Common) {
|
|
||||||
return std::make_pair(rights_id, ticket.GetData().title_key_common);
|
|
||||||
}
|
|
||||||
|
|
||||||
mbedtls_mpi D; // RSA Private Exponent
|
|
||||||
mbedtls_mpi N; // RSA Modulus
|
|
||||||
mbedtls_mpi S; // Input
|
|
||||||
mbedtls_mpi M; // Output
|
|
||||||
|
|
||||||
mbedtls_mpi_init(&D);
|
|
||||||
mbedtls_mpi_init(&N);
|
|
||||||
mbedtls_mpi_init(&S);
|
|
||||||
mbedtls_mpi_init(&M);
|
|
||||||
|
|
||||||
mbedtls_mpi_read_binary(&D, key.decryption_key.data(), key.decryption_key.size());
|
|
||||||
mbedtls_mpi_read_binary(&N, key.modulus.data(), key.modulus.size());
|
|
||||||
mbedtls_mpi_read_binary(&S, ticket.GetData().title_key_block.data(), 0x100);
|
|
||||||
|
|
||||||
mbedtls_mpi_exp_mod(&M, &S, &D, &N, nullptr);
|
|
||||||
|
|
||||||
std::array<u8, 0x100> rsa_step;
|
|
||||||
mbedtls_mpi_write_binary(&M, rsa_step.data(), rsa_step.size());
|
|
||||||
|
|
||||||
u8 m_0 = rsa_step[0];
|
|
||||||
std::array<u8, 0x20> m_1;
|
|
||||||
std::memcpy(m_1.data(), rsa_step.data() + 0x01, m_1.size());
|
|
||||||
std::array<u8, 0xDF> m_2;
|
|
||||||
std::memcpy(m_2.data(), rsa_step.data() + 0x21, m_2.size());
|
|
||||||
|
|
||||||
if (m_0 != 0) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_1 = m_1 ^ MGF1<0x20>(m_2);
|
|
||||||
m_2 = m_2 ^ MGF1<0xDF>(m_1);
|
|
||||||
|
|
||||||
const auto offset = FindTicketOffset(m_2);
|
|
||||||
if (!offset) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
ASSERT(*offset > 0);
|
|
||||||
|
|
||||||
Key128 key_temp{};
|
|
||||||
std::memcpy(key_temp.data(), m_2.data() + *offset, key_temp.size());
|
|
||||||
|
|
||||||
return std::make_pair(rights_id, key_temp);
|
|
||||||
} catch (const std::bad_variant_access&) {
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
if (issuer[0] != 'R' || issuer[1] != 'o' || issuer[2] != 'o' || issuer[3] != 't') {
|
||||||
|
LOG_INFO(Crypto, "Attempting to parse ticket with non-standard certificate authority.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Key128 rights_id = ticket.GetData().rights_id;
|
||||||
|
|
||||||
|
if (rights_id == Key128{}) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ticket.GetData().type == TitleKeyType::Common) {
|
||||||
|
return std::make_pair(rights_id, ticket.GetData().title_key_common);
|
||||||
|
}
|
||||||
|
|
||||||
|
mbedtls_mpi D; // RSA Private Exponent
|
||||||
|
mbedtls_mpi N; // RSA Modulus
|
||||||
|
mbedtls_mpi S; // Input
|
||||||
|
mbedtls_mpi M; // Output
|
||||||
|
|
||||||
|
mbedtls_mpi_init(&D);
|
||||||
|
mbedtls_mpi_init(&N);
|
||||||
|
mbedtls_mpi_init(&S);
|
||||||
|
mbedtls_mpi_init(&M);
|
||||||
|
|
||||||
|
mbedtls_mpi_read_binary(&D, key.decryption_key.data(), key.decryption_key.size());
|
||||||
|
mbedtls_mpi_read_binary(&N, key.modulus.data(), key.modulus.size());
|
||||||
|
mbedtls_mpi_read_binary(&S, ticket.GetData().title_key_block.data(), 0x100);
|
||||||
|
|
||||||
|
mbedtls_mpi_exp_mod(&M, &S, &D, &N, nullptr);
|
||||||
|
|
||||||
|
std::array<u8, 0x100> rsa_step;
|
||||||
|
mbedtls_mpi_write_binary(&M, rsa_step.data(), rsa_step.size());
|
||||||
|
|
||||||
|
u8 m_0 = rsa_step[0];
|
||||||
|
std::array<u8, 0x20> m_1;
|
||||||
|
std::memcpy(m_1.data(), rsa_step.data() + 0x01, m_1.size());
|
||||||
|
std::array<u8, 0xDF> m_2;
|
||||||
|
std::memcpy(m_2.data(), rsa_step.data() + 0x21, m_2.size());
|
||||||
|
|
||||||
|
if (m_0 != 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_1 = m_1 ^ MGF1<0x20>(m_2);
|
||||||
|
m_2 = m_2 ^ MGF1<0xDF>(m_1);
|
||||||
|
|
||||||
|
const auto offset = FindTicketOffset(m_2);
|
||||||
|
if (!offset) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
ASSERT(*offset > 0);
|
||||||
|
|
||||||
|
Key128 key_temp{};
|
||||||
|
std::memcpy(key_temp.data(), m_2.data() + *offset, key_temp.size());
|
||||||
|
|
||||||
|
return std::make_pair(rights_id, key_temp);
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyManager::KeyManager() {
|
KeyManager::KeyManager() {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
@ -80,6 +81,7 @@ struct RSA4096Ticket {
|
||||||
INSERT_PADDING_BYTES(0x3C);
|
INSERT_PADDING_BYTES(0x3C);
|
||||||
TicketData data;
|
TicketData data;
|
||||||
};
|
};
|
||||||
|
static_assert(sizeof(RSA4096Ticket) == 0x500, "RSA4096Ticket has incorrect size.");
|
||||||
|
|
||||||
struct RSA2048Ticket {
|
struct RSA2048Ticket {
|
||||||
SignatureType sig_type;
|
SignatureType sig_type;
|
||||||
|
@ -87,6 +89,7 @@ struct RSA2048Ticket {
|
||||||
INSERT_PADDING_BYTES(0x3C);
|
INSERT_PADDING_BYTES(0x3C);
|
||||||
TicketData data;
|
TicketData data;
|
||||||
};
|
};
|
||||||
|
static_assert(sizeof(RSA2048Ticket) == 0x400, "RSA2048Ticket has incorrect size.");
|
||||||
|
|
||||||
struct ECDSATicket {
|
struct ECDSATicket {
|
||||||
SignatureType sig_type;
|
SignatureType sig_type;
|
||||||
|
@ -94,18 +97,41 @@ struct ECDSATicket {
|
||||||
INSERT_PADDING_BYTES(0x40);
|
INSERT_PADDING_BYTES(0x40);
|
||||||
TicketData data;
|
TicketData data;
|
||||||
};
|
};
|
||||||
|
static_assert(sizeof(ECDSATicket) == 0x340, "ECDSATicket has incorrect size.");
|
||||||
|
|
||||||
struct Ticket {
|
struct Ticket {
|
||||||
std::variant<std::monostate, RSA4096Ticket, RSA2048Ticket, ECDSATicket> data;
|
std::variant<std::monostate, RSA4096Ticket, RSA2048Ticket, ECDSATicket> data;
|
||||||
|
|
||||||
bool IsValid() const;
|
[[nodiscard]] bool IsValid() const;
|
||||||
SignatureType GetSignatureType() const;
|
[[nodiscard]] SignatureType GetSignatureType() const;
|
||||||
TicketData& GetData();
|
[[nodiscard]] TicketData& GetData();
|
||||||
const TicketData& GetData() const;
|
[[nodiscard]] const TicketData& GetData() const;
|
||||||
u64 GetSize() const;
|
[[nodiscard]] u64 GetSize() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synthesizes a common ticket given a title key and rights ID.
|
||||||
|
*
|
||||||
|
* @param title_key Title key to store in the ticket.
|
||||||
|
* @param rights_id Rights ID the ticket is for.
|
||||||
|
* @return The synthesized common ticket.
|
||||||
|
*/
|
||||||
static Ticket SynthesizeCommon(Key128 title_key, const std::array<u8, 0x10>& rights_id);
|
static Ticket SynthesizeCommon(Key128 title_key, const std::array<u8, 0x10>& rights_id);
|
||||||
static bool Read(Ticket& ticket_out, const FileSys::VirtualFile& file);
|
|
||||||
|
/**
|
||||||
|
* Reads a ticket from a file.
|
||||||
|
*
|
||||||
|
* @param file File to read the ticket from.
|
||||||
|
* @return The read ticket. If the ticket data is invalid, Ticket::IsValid() will be false.
|
||||||
|
*/
|
||||||
|
static Ticket Read(const FileSys::VirtualFile& file);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a ticket from a memory buffer.
|
||||||
|
*
|
||||||
|
* @param raw_data Buffer to read the ticket from.
|
||||||
|
* @return The read ticket. If the ticket data is invalid, Ticket::IsValid() will be false.
|
||||||
|
*/
|
||||||
|
static Ticket Read(std::span<const u8> raw_data);
|
||||||
};
|
};
|
||||||
|
|
||||||
static_assert(sizeof(Key128) == 16, "Key128 must be 128 bytes big.");
|
static_assert(sizeof(Key128) == 16, "Key128 must be 128 bytes big.");
|
||||||
|
|
|
@ -190,8 +190,8 @@ void NSP::SetTicketKeys(const std::vector<VirtualFile>& files) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Core::Crypto::Ticket ticket{};
|
auto ticket = Core::Crypto::Ticket::Read(ticket_file);
|
||||||
if (!Core::Crypto::Ticket::Read(ticket, ticket_file)) {
|
if (!ticket.IsValid()) {
|
||||||
LOG_WARNING(Common_Filesystem, "Could not read NSP ticket {}", ticket_file->GetName());
|
LOG_WARNING(Common_Filesystem, "Could not read NSP ticket {}", ticket_file->GetName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,20 +122,25 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImportTicket(HLERequestContext& ctx) {
|
void ImportTicket(HLERequestContext& ctx) {
|
||||||
const auto ticket = ctx.ReadBuffer();
|
const auto raw_ticket = ctx.ReadBuffer();
|
||||||
[[maybe_unused]] const auto cert = ctx.ReadBuffer(1);
|
[[maybe_unused]] const auto cert = ctx.ReadBuffer(1);
|
||||||
|
|
||||||
if (ticket.size() < sizeof(Core::Crypto::Ticket)) {
|
if (raw_ticket.size() < sizeof(Core::Crypto::Ticket)) {
|
||||||
LOG_ERROR(Service_ETicket, "The input buffer is not large enough!");
|
LOG_ERROR(Service_ETicket, "The input buffer is not large enough!");
|
||||||
IPC::ResponseBuilder rb{ctx, 2};
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
rb.Push(ERROR_INVALID_ARGUMENT);
|
rb.Push(ERROR_INVALID_ARGUMENT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Core::Crypto::Ticket raw{};
|
Core::Crypto::Ticket ticket = Core::Crypto::Ticket::Read(raw_ticket);
|
||||||
std::memcpy(&raw, ticket.data(), sizeof(Core::Crypto::Ticket));
|
if (!ticket.IsValid()) {
|
||||||
|
LOG_ERROR(Service_ETicket, "The ticket is invalid!");
|
||||||
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(ERROR_INVALID_ARGUMENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!keys.AddTicketPersonalized(raw)) {
|
if (!keys.AddTicketPersonalized(ticket)) {
|
||||||
LOG_ERROR(Service_ETicket, "The ticket could not be imported!");
|
LOG_ERROR(Service_ETicket, "The ticket could not be imported!");
|
||||||
IPC::ResponseBuilder rb{ctx, 2};
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
rb.Push(ERROR_INVALID_ARGUMENT);
|
rb.Push(ERROR_INVALID_ARGUMENT);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue