Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IOS/ES: Add signature verification #5612

Merged
merged 12 commits into from
Jun 16, 2017
2 changes: 2 additions & 0 deletions Source/Core/Core/ConfigManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ void SConfig::SaveCoreSettings(IniFile& ini)
core->Set("PerfMapDir", m_perfDir);
core->Set("EnableCustomRTC", bEnableCustomRTC);
core->Set("CustomRTCValue", m_customRTCValue);
core->Set("EnableSignatureChecks", m_enable_signature_checks);
}

void SConfig::SaveMovieSettings(IniFile& ini)
Expand Down Expand Up @@ -608,6 +609,7 @@ void SConfig::LoadCoreSettings(IniFile& ini)
core->Get("EnableCustomRTC", &bEnableCustomRTC, false);
// Default to seconds between 1.1.1970 and 1.1.2000
core->Get("CustomRTCValue", &m_customRTCValue, 946684800);
core->Get("EnableSignatureChecks", &m_enable_signature_checks, true);
}

void SConfig::LoadMovieSettings(IniFile& ini)
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/Core/ConfigManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ struct SConfig : NonCopyable
std::set<std::pair<u16, u16>> m_usb_passthrough_devices;
bool IsUSBDeviceWhitelisted(std::pair<u16, u16> vid_pid) const;

bool m_enable_signature_checks = true;

// SYSCONF settings
int m_sensor_bar_position = 0x01;
int m_sensor_bar_sensitivity = 0x03;
Expand Down
53 changes: 27 additions & 26 deletions Source/Core/Core/IOS/Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,39 @@ namespace HLE
{
enum ReturnCode : s32
{
IPC_SUCCESS = 0, // Success
IPC_EACCES = -1, // Permission denied
IPC_EEXIST = -2, // File exists
IPC_EINVAL = -4, // Invalid argument or fd
IPC_EMAX = -5, // Too many file descriptors open
IPC_ENOENT = -6, // File not found
IPC_EQUEUEFULL = -8, // Queue full
IPC_EIO = -12, // ECC error
IPC_ENOMEM = -22, // Alloc failed during request
FS_EINVAL = -101, // Invalid path
FS_EACCESS = -102, // Permission denied
FS_ECORRUPT = -103, // Corrupted NAND
FS_EEXIST = -105, // File exists
FS_ENOENT = -106, // No such file or directory
FS_ENFILE = -107, // Too many fds open
FS_EFBIG = -108, // Max block count reached?
FS_EFDEXHAUSTED = -109, // Too many fds open
FS_ENAMELEN = -110, // Pathname is too long
FS_EFDOPEN = -111, // FD is already open
FS_EIO = -114, // ECC error
FS_ENOTEMPTY = -115, // Directory not empty
FS_EDIRDEPTH = -116, // Max directory depth exceeded
FS_EBUSY = -118, // Resource busy
ES_SHORT_READ = -1009, // Short read
ES_EIO = -1010, // Write failure
IPC_SUCCESS = 0, // Success
IPC_EACCES = -1, // Permission denied
IPC_EEXIST = -2, // File exists
IPC_EINVAL = -4, // Invalid argument or fd
IPC_EMAX = -5, // Too many file descriptors open
IPC_ENOENT = -6, // File not found
IPC_EQUEUEFULL = -8, // Queue full
IPC_EIO = -12, // ECC error
IPC_ENOMEM = -22, // Alloc failed during request
FS_EINVAL = -101, // Invalid path
FS_EACCESS = -102, // Permission denied
FS_ECORRUPT = -103, // Corrupted NAND
FS_EEXIST = -105, // File exists
FS_ENOENT = -106, // No such file or directory
FS_ENFILE = -107, // Too many fds open
FS_EFBIG = -108, // Max block count reached?
FS_EFDEXHAUSTED = -109, // Too many fds open
FS_ENAMELEN = -110, // Pathname is too long
FS_EFDOPEN = -111, // FD is already open
FS_EIO = -114, // ECC error
FS_ENOTEMPTY = -115, // Directory not empty
FS_EDIRDEPTH = -116, // Max directory depth exceeded
FS_EBUSY = -118, // Resource busy
ES_SHORT_READ = -1009, // Short read
ES_EIO = -1010, // Write failure
ES_INVALID_SIGNATURE_TYPE = -1012,
ES_FD_EXHAUSTED = -1016, // Max of 3 ES handles exceeded
ES_EINVAL = -1017, // Invalid argument
ES_DEVICE_ID_MISMATCH = -1018,
ES_HASH_MISMATCH = -1022, // Decrypted content hash doesn't match with the hash from the TMD
ES_ENOMEM = -1024, // Alloc failed during request
ES_EACCES = -1026, // Incorrect access rights (according to TMD)
ES_INVALID_TMD_SIGNATURE_TYPE = -1027,
ES_UNKNOWN_ISSUER = -1027,
ES_NO_TICKET = -1028,
ES_INVALID_TICKET = -1029,
IOSC_EACCES = -2000,
Expand Down
204 changes: 204 additions & 0 deletions Source/Core/Core/IOS/ES/ES.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@
#include <utility>
#include <vector>

#include <mbedtls/sha1.h>

#include "Common/ChunkFile.h"
#include "Common/File.h"
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/MsgHandler.h"
#include "Common/NandPaths.h"
#include "Common/ScopeGuard.h"
#include "Common/StringUtil.h"
#include "Core/ConfigManager.h"
#include "Core/HW/Memmap.h"
#include "Core/IOS/ES/Formats.h"
#include "Core/IOS/IOSC.h"
#include "DiscIO/NANDContentLoader.h"

namespace IOS
Expand All @@ -35,8 +40,46 @@ static TitleContext s_title_context;
// Title to launch after IOS has been reset and reloaded (similar to /sys/launch.sys).
static u64 s_title_to_launch;

struct DirectoryToCreate
{
const char* path;
u32 attributes;
OpenMode owner_perm;
OpenMode group_perm;
OpenMode other_perm;
};

constexpr std::array<DirectoryToCreate, 9> s_directories_to_create = {{
{"/sys", 0, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_NONE},
{"/ticket", 0, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_NONE},
{"/title", 0, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_READ},
{"/shared1", 0, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_NONE},
{"/shared2", 0, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_RW},
{"/tmp", 0, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_RW},
{"/import", 0, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_NONE},
{"/meta", 0, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_RW},
{"/wfs", 0, OpenMode::IOS_OPEN_RW, OpenMode::IOS_OPEN_NONE, OpenMode::IOS_OPEN_NONE},
}};

ES::ES(Kernel& ios, const std::string& device_name) : Device(ios, device_name)
{
for (const auto& directory : s_directories_to_create)
{
const std::string path = Common::RootUserPath(Common::FROM_SESSION_ROOT) + directory.path;

// Create the directory if it does not exist.
if (File::IsDirectory(path))
continue;

File::CreateFullPath(path);
if (File::CreateDir(path))
INFO_LOG(IOS_ES, "Created %s (at %s)", directory.path, path.c_str());
else
ERROR_LOG(IOS_ES, "Failed to create %s (at %s)", directory.path, path.c_str());

// TODO: Set permissions.
}

FinishAllStaleImports();

s_content_file = "";
Expand Down Expand Up @@ -610,6 +653,9 @@ s32 ES::DIVerify(const IOS::ES::TMDReader& tmd, const IOS::ES::TicketReader& tic

if (!File::Exists(tmd_path))
{
// XXX: We are supposed to verify the TMD and ticket here, but cannot because
// this may cause issues with custom/patched games.

File::IOFile tmd_file(tmd_path, "wb");
const std::vector<u8>& tmd_bytes = tmd.GetBytes();
if (!tmd_file.WriteBytes(tmd_bytes.data(), tmd_bytes.size()))
Expand Down Expand Up @@ -692,6 +738,19 @@ ReturnCode ES::SetUpStreamKey(const u32 uid, const u8* ticket_view, const IOS::E
if (ticket_bytes.empty())
return ES_NO_TICKET;

std::vector<u8> cert_store;
ret = ReadCertStore(&cert_store);
if (ret != IPC_SUCCESS)
return ret;

ret = VerifyContainer(VerifyContainerType::TMD, VerifyMode::UpdateCertStore, tmd, cert_store);
if (ret != IPC_SUCCESS)
return ret;
ret = VerifyContainer(VerifyContainerType::Ticket, VerifyMode::UpdateCertStore, installed_ticket,
cert_store);
if (ret != IPC_SUCCESS)
return ret;

// Create the handle and return it.
std::array<u8, 16> iv{};
std::memcpy(iv.data(), &title_id, sizeof(title_id));
Expand Down Expand Up @@ -760,6 +819,151 @@ bool ES::IsActiveTitlePermittedByTicket(const u8* ticket_view) const
Common::swap32(ticket_view + offsetof(IOS::ES::TicketView, permitted_title_id));
return title_identifier && (title_identifier & ~permitted_title_mask) == permitted_title_id;
}

bool ES::IsIssuerCorrect(VerifyContainerType type, const IOS::ES::CertReader& issuer_cert) const
{
switch (type)
{
case VerifyContainerType::TMD:
return issuer_cert.GetName().compare(0, 2, "CP") == 0;
case VerifyContainerType::Ticket:
return issuer_cert.GetName().compare(0, 2, "XS") == 0;
case VerifyContainerType::Device:
return issuer_cert.GetName().compare(0, 2, "MS") == 0;
default:
return false;
}
}

ReturnCode ES::ReadCertStore(std::vector<u8>* buffer) const
{
const std::string store_path = Common::RootUserPath(Common::FROM_SESSION_ROOT) + "/sys/cert.sys";
File::IOFile store_file{store_path, "rb"};
if (!store_file)
return FS_ENOENT;

buffer->resize(store_file.GetSize());
if (!store_file.ReadBytes(buffer->data(), buffer->size()))
return ES_SHORT_READ;
return IPC_SUCCESS;
}

ReturnCode ES::WriteNewCertToStore(const IOS::ES::CertReader& cert)
{
const std::string store_path = Common::RootUserPath(Common::FROM_SESSION_ROOT) + "/sys/cert.sys";
// The certificate store file may not exist, so we use a+b and not r+b here.
File::IOFile store_file{store_path, "a+b"};
if (!store_file)
return ES_EIO;

// Read the current store to determine if the new cert needs to be written.
const u64 file_size = store_file.GetSize();
if (file_size != 0)
{
std::vector<u8> certs_bytes(file_size);
if (!store_file.ReadBytes(certs_bytes.data(), certs_bytes.size()))
return ES_SHORT_READ;

const std::map<std::string, IOS::ES::CertReader> certs = IOS::ES::ParseCertChain(certs_bytes);
// The cert is already present in the store. Nothing to do.
if (certs.find(cert.GetName()) != certs.end())
return IPC_SUCCESS;
}

// Otherwise, write the new cert at the end of the store.
// When opening a file in read-write mode, a seek is required before a write.
store_file.Seek(0, SEEK_END);
if (!store_file.WriteBytes(cert.GetBytes().data(), cert.GetBytes().size()))
return ES_EIO;
return IPC_SUCCESS;
}

ReturnCode ES::VerifyContainer(VerifyContainerType type, VerifyMode mode,
const IOS::ES::SignedBlobReader& signed_blob,
const std::vector<u8>& cert_chain, u32 iosc_handle)
{
if (!SConfig::GetInstance().m_enable_signature_checks)
return IPC_SUCCESS;

if (!signed_blob.IsSignatureValid())
return ES_EINVAL;

// A blob should have exactly 3 parent issuers.
// Example for a ticket: "Root-CA00000001-XS00000003" => {"Root", "CA00000001", "XS00000003"}
const std::string issuer = signed_blob.GetIssuer();
const std::vector<std::string> parents = SplitString(issuer, '-');
if (parents.size() != 3)
return ES_EINVAL;

// Find the direct issuer and the CA certificates for the blob.
const std::map<std::string, IOS::ES::CertReader> certs = IOS::ES::ParseCertChain(cert_chain);
const auto issuer_cert_iterator = certs.find(parents[2]);
const auto ca_cert_iterator = certs.find(parents[1]);
if (issuer_cert_iterator == certs.end() || ca_cert_iterator == certs.end())
return ES_UNKNOWN_ISSUER;
const IOS::ES::CertReader& issuer_cert = issuer_cert_iterator->second;
const IOS::ES::CertReader& ca_cert = ca_cert_iterator->second;

// Some blobs can only be signed by specific certificates.
if (!IsIssuerCorrect(type, issuer_cert))
return ES_EINVAL;

// Verify the whole cert chain using IOSC.
// IOS assumes that the CA cert will always be signed by the root certificate,
// and that the issuer is signed by the CA.
IOSC& iosc = m_ios.GetIOSC();
IOSC::Handle handle;

// Create and initialise a handle for the CA cert and the issuer cert.
ReturnCode ret = iosc.CreateObject(&handle, IOSC::TYPE_PUBLIC_KEY, IOSC::SUBTYPE_RSA2048, PID_ES);
if (ret != IPC_SUCCESS)
return ret;
Common::ScopeGuard ca_guard{[&] { iosc.DeleteObject(handle, PID_ES); }};
ret = iosc.ImportCertificate(ca_cert.GetBytes().data(), IOSC::HANDLE_ROOT_KEY, handle, PID_ES);
if (ret != IPC_SUCCESS)
return ret;

IOSC::Handle issuer_handle;
const IOSC::ObjectSubType subtype =
type == VerifyContainerType::Device ? IOSC::SUBTYPE_ECC233 : IOSC::SUBTYPE_RSA2048;
ret = iosc.CreateObject(&issuer_handle, IOSC::TYPE_PUBLIC_KEY, subtype, PID_ES);
if (ret != IPC_SUCCESS)
return ret;
Common::ScopeGuard issuer_guard{[&] { iosc.DeleteObject(issuer_handle, PID_ES); }};
ret = iosc.ImportCertificate(issuer_cert.GetBytes().data(), handle, issuer_handle, PID_ES);
if (ret != IPC_SUCCESS)
return ret;

// Calculate the SHA1 of the signed blob.
const size_t skip = type == VerifyContainerType::Device ? offsetof(SignatureECC, issuer) :
offsetof(SignatureRSA2048, issuer);
std::array<u8, 20> sha1;
mbedtls_sha1(signed_blob.GetBytes().data() + skip, signed_blob.GetBytes().size() - skip,
sha1.data());

// Verify the signature.
const std::vector<u8> signature = signed_blob.GetSignatureData();
ret = iosc.VerifyPublicKeySign(sha1, issuer_handle, signature.data(), PID_ES);
if (ret != IPC_SUCCESS)
return ret;

if (mode == VerifyMode::UpdateCertStore)
{
ret = WriteNewCertToStore(issuer_cert);
if (ret != IPC_SUCCESS)
ERROR_LOG(IOS_ES, "VerifyContainer: Writing the issuer cert failed with return code %d", ret);

ret = WriteNewCertToStore(ca_cert);
if (ret != IPC_SUCCESS)
ERROR_LOG(IOS_ES, "VerifyContainer: Writing the CA cert failed with return code %d", ret);
}

// Import the signed blob to iosc_handle (if a handle was passed to us).
if (ret == IPC_SUCCESS && iosc_handle)
ret = iosc.ImportCertificate(signed_blob.GetBytes().data(), issuer_handle, iosc_handle, PID_ES);

return ret;
}
} // namespace Device
} // namespace HLE
} // namespace IOS
24 changes: 22 additions & 2 deletions Source/Core/Core/IOS/ES/ES.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,10 @@ class ES final : public Device
std::vector<std::array<u8, 20>> GetSharedContents() const;

// Title management
ReturnCode ImportTicket(const std::vector<u8>& ticket_bytes);
ReturnCode ImportTicket(const std::vector<u8>& ticket_bytes, const std::vector<u8>& cert_chain);
ReturnCode ImportTmd(Context& context, const std::vector<u8>& tmd_bytes);
ReturnCode ImportTitleInit(Context& context, const std::vector<u8>& tmd_bytes);
ReturnCode ImportTitleInit(Context& context, const std::vector<u8>& tmd_bytes,
const std::vector<u8>& cert_chain);
ReturnCode ImportContentBegin(Context& context, u64 title_id, u32 content_id);
ReturnCode ImportContentData(Context& context, u32 content_fd, const u8* data, u32 data_size);
ReturnCode ImportContentEnd(Context& context, u32 content_fd);
Expand Down Expand Up @@ -306,6 +307,25 @@ class ES final : public Device
ReturnCode CheckStreamKeyPermissions(u32 uid, const u8* ticket_view,
const IOS::ES::TMDReader& tmd) const;

enum class VerifyContainerType
{
TMD,
Ticket,
Device,
};
enum class VerifyMode
{
// Whether or not new certificates should be added to the certificate store (/sys/cert.sys).
DoNotUpdateCertStore,
UpdateCertStore,
};
bool IsIssuerCorrect(VerifyContainerType type, const IOS::ES::CertReader& issuer_cert) const;
ReturnCode ReadCertStore(std::vector<u8>* buffer) const;
ReturnCode WriteNewCertToStore(const IOS::ES::CertReader& cert);
ReturnCode VerifyContainer(VerifyContainerType type, VerifyMode mode,
const IOS::ES::SignedBlobReader& signed_blob,
const std::vector<u8>& cert_chain, u32 iosc_handle = 0);

// Start a title import.
bool InitImport(u64 title_id);
// Clean up the import content directory and move it back to /title.
Expand Down