Validate tickets via RSA signature

-> only proper / signed tickets in T:/ drive
-> 100% safe way of telling a ticket is legit
This commit is contained in:
d0k3 2018-01-10 02:08:29 +01:00
parent 5d8758bf83
commit 24c31f482d
6 changed files with 261 additions and 6 deletions

119
arm9/source/crypto/rsa.c Normal file
View File

@ -0,0 +1,119 @@
/*
* This file is part of fastboot 3DS
* Copyright (C) 2017 derrek, profi200
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "common.h"
#include "rsa.h"
#include "sha.h"
//////////////////////////////////
// RSA //
//////////////////////////////////
#define RSA_REGS_BASE (0x10000000 + 0xB000)
#define REG_RSA_CNT *((vu32*)(RSA_REGS_BASE + 0x000))
#define REG_RSA_UNK_F0 *((vu32*)(RSA_REGS_BASE + 0x0F0))
#define REGs_RSA_SLOT0 ((vu32*)(RSA_REGS_BASE + 0x100))
#define REGs_RSA_SLOT1 ((vu32*)(RSA_REGS_BASE + 0x110))
#define REGs_RSA_SLOT2 ((vu32*)(RSA_REGS_BASE + 0x120))
#define REGs_RSA_SLOT3 ((vu32*)(RSA_REGS_BASE + 0x130))
#define rsaSlots ((RsaSlot*)(RSA_REGS_BASE + 0x100))
#define REG_RSA_EXP ((vu32*)(RSA_REGS_BASE + 0x200))
#define REG_RSA_MOD ( (RSA_REGS_BASE + 0x400))
#define REG_RSA_TXT ( (RSA_REGS_BASE + 0x800))
typedef struct
{
vu32 REG_RSA_SLOTCNT;
vu32 REG_RSA_SLOTSIZE;
vu32 REG_RSA_SLOT_UNK_0x8;
vu32 REG_RSA_SLOT_UNK_0xC;
} RsaSlot;
void RSA_init(void)
{
REG_RSA_UNK_F0 = 0;
}
static void rsaWaitBusy(void)
{
while(REG_RSA_CNT & RSA_ENABLE);
}
void RSA_selectKeyslot(u8 keyslot)
{
rsaWaitBusy();
REG_RSA_CNT = (REG_RSA_CNT & ~RSA_KEYSLOT(0xFu)) | RSA_KEYSLOT(keyslot);
}
bool RSA_setKey2048(u8 keyslot, const u8 *const mod, u32 exp)
{
RsaSlot *slot = &rsaSlots[keyslot];
rsaWaitBusy();
if(slot->REG_RSA_SLOTCNT & RSA_KEY_WR_PROT) return false;
// Unset key if bit 31 is not set. No idea why but boot9 does this.
if(!(slot->REG_RSA_SLOTCNT & RSA_KEY_UNK_BIT31)) slot->REG_RSA_SLOTCNT &= ~RSA_KEY_STAT_SET;
REG_RSA_CNT = RSA_INPUT_NORMAL | RSA_INPUT_BIG | RSA_KEYSLOT(keyslot);
memset((void*)REG_RSA_EXP, 0, 0x100 - 4);
REG_RSA_EXP[(0x100>>2) - 1] = exp;
if(slot->REG_RSA_SLOTSIZE != RSA_SLOTSIZE_2048) return false;
memcpy((void*)REG_RSA_MOD, mod, 0x100);
return true;
}
bool RSA_decrypt2048(void *const decSig, const void *const encSig)
{
const u8 keyslot = RSA_GET_KEYSLOT;
rsaWaitBusy();
if(!(rsaSlots[keyslot].REG_RSA_SLOTCNT & RSA_KEY_STAT_SET)) return false;
REG_RSA_CNT |= RSA_INPUT_NORMAL | RSA_INPUT_BIG;
memcpy((void*)REG_RSA_TXT, encSig, 0x100);
REG_RSA_CNT |= RSA_ENABLE;
rsaWaitBusy();
memcpy(decSig, (void*)REG_RSA_TXT, 0x100);
return true;
}
bool RSA_verify2048(const u32 *const encSig, const u32 *const data, u32 size)
{
u8 decSig[0x100];
if(!RSA_decrypt2048(decSig, encSig)) return false;
if(decSig[0] != 0x00 || decSig[1] != 0x01) return false;
u32 read = 2;
while(read < 0x100)
{
if(decSig[read] != 0xFF) break;
read++;
}
if(read != 0xCC || decSig[read] != 0x00) return false;
// ASN.1 is a clusterfuck so we skip parsing the remaining headers
// and hardcode the hash location.
return sha_cmp(&(decSig[0xE0]), data, size, SHA256_MODE) == 0;
}

91
arm9/source/crypto/rsa.h Normal file
View File

@ -0,0 +1,91 @@
#pragma once
/*
* This file is part of fastboot 3DS
* Copyright (C) 2017 derrek, profi200
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "common.h"
//////////////////////////////////
// RSA //
//////////////////////////////////
// REG_RSA_CNT
#define RSA_ENABLE (1u)
#define RSA_UNK_BIT1 (1u<<1)
#define RSA_KEYSLOT(k) ((k)<<4)
#define RSA_GET_KEYSLOT ((REG_RSA_CNT & RSA_KEYSLOT(0xFu))>>4)
#define RSA_INPUT_BIG (1u<<8)
#define RSA_INPUT_LITTLE (0u)
#define RSA_INPUT_NORMAL (1u<<9)
#define RSA_INPUT_REVERSED (0u)
// RSA_SLOTCNT
#define RSA_KEY_STAT_SET (1u)
#define RSA_KEY_WR_PROT (1u<<1)
#define RSA_KEY_UNK_BIT31 (1u<<31)
// RSA_SLOTSIZE
#define RSA_SLOTSIZE_2048 (0x40u)
/**
* @brief Initializes the RSA hardware.
*/
void RSA_init(void);
/**
* @brief Selects the given keyslot for all following RSA operations.
*
* @param[in] keyslot The keyslot to select.
*/
void RSA_selectKeyslot(u8 keyslot);
/**
* @brief Sets a RSA modulus + exponent in the specified keyslot.
*
* @param[in] keyslot The keyslot this key will be set for.
* @param[in] mod Pointer to 2048-bit RSA modulus data.
* @param[in] exp The exponent to set.
*
* @return Returns true on success, false otherwise.
*/
bool RSA_setKey2048(u8 keyslot, const u8 *const mod, u32 exp);
/**
* @brief Decrypts a RSA 2048 signature.
*
* @param decSig Pointer to decrypted destination signature.
* @param[in] encSig Pointer to encrypted source signature.
*
* @return Returns true on success, false otherwise.
*/
bool RSA_decrypt2048(void *const decSig, const void *const encSig);
/**
* @brief Verifies a RSA 2048 SHA 256 signature.
* @brief Note: This function skips the ASN.1 data and is therefore not safe.
*
* @param[in] encSig Pointer to encrypted source signature.
* @param[in] data Pointer to the data to hash.
* @param[in] size The hash data size.
*
* @return Returns true if the signature is valid, false otherwise.
*/
bool RSA_verify2048(const u32 *const encSig, const u32 *const data, u32 size);

View File

@ -3,6 +3,7 @@
#include "unittype.h"
#include "aes.h"
#include "sha.h"
#include "rsa.h"
#include "ff.h"
u32 ValidateTicket(Ticket* ticket) {
@ -15,6 +16,33 @@ u32 ValidateTicket(Ticket* ticket) {
return 0;
}
u32 ValidateTicketSignature(Ticket* ticket) {
static bool got_modexp = false;
static u8 mod[0x100] = { 0 };
static u32 exp = 0;
// grab cert from cert.db
if (!got_modexp) {
Certificate cert;
FIL db;
UINT bytes_read;
if (f_open(&db, "1:/dbs/certs.db", FA_READ | FA_OPEN_EXISTING) != FR_OK)
return 1;
f_lseek(&db, 0x3F10);
f_read(&db, &cert, CERT_SIZE, &bytes_read);
f_close(&db);
memcpy(mod, cert.mod, 0x100);
exp = getle32(cert.exp);
got_modexp = true;
}
if (!RSA_setKey2048(3, mod, exp) ||
!RSA_verify2048((void*) &(ticket->signature), (void*) &(ticket->issuer), 0x210))
return 1;
return 0;
}
u32 CryptTitleKey(TitleKeyEntry* tik, bool encrypt, bool devkit) {
// From https://github.com/profi200/Project_CTR/blob/master/makerom/pki/prod.h#L19
static const u8 common_keyy[6][16] __attribute__((aligned(16))) = {
@ -68,7 +96,7 @@ Ticket* TicketFromTickDbChunk(u8* chunk, u8* title_id, bool legit_pls) {
if ((getle32(chunk + 0x10) == 0) || (getle32(chunk + 0x14) != sizeof(Ticket))) return NULL;
if (ValidateTicket(tick) != 0) return NULL; // ticket not validated
if (title_id && (memcmp(title_id, tick->title_id, 8) != 0)) return NULL; // title id not matching
if (legit_pls && (getbe64(tick->ticket_id) == 0)) return NULL; // legit check, not perfect
if (legit_pls && (ValidateTicketSignature(tick) != 0)) return NULL; // legit check using RSA sig
return tick;
}
@ -195,7 +223,7 @@ u32 BuildTicketCert(u8* tickcert) {
UINT bytes_read;
if (f_open(&db, "1:/dbs/certs.db", FA_READ | FA_OPEN_EXISTING) != FR_OK)
return 1;
// grab Ticket cert from 3 offsets
// grab ticket cert from 3 offsets
f_lseek(&db, 0x3F10);
f_read(&db, tickcert + 0x000, 0x300, &bytes_read);
f_lseek(&db, 0x0C10);

View File

@ -3,6 +3,7 @@
#include "common.h"
#define TICKET_SIZE sizeof(Ticket)
#define CERT_SIZE sizeof(Certificate)
#define TICKET_CDNCERT_SIZE 0x700
#define TICKET_ISSUER "Root-CA00000003-XS0000000c"
@ -27,6 +28,7 @@
0x00, 0xEE, 0x37, 0x02, 0x00, 0x00, 0x00, 0x00
// from: https://github.com/profi200/Project_CTR/blob/02159e17ee225de3f7c46ca195ff0f9ba3b3d3e4/ctrtool/tik.h#L15-L39
// all numbers in big endian
typedef struct {
u8 sig_type[4];
u8 signature[0x100];
@ -57,6 +59,21 @@ typedef struct {
u8 content_index[0xAC];
} __attribute__((packed)) Ticket;
// from: http://3dbrew.org/wiki/Certificates
// all numbers in big endian
typedef struct {
u8 sig_type[4]; // expected: 0x010004 / RSA_2048 SHA256
u8 signature[0x100];
u8 padding0[0x3C];
u8 issuer[0x40];
u8 keytype[4]; // expected: 0x01 / RSA_2048
u8 name[0x40];
u8 unknown[4];
u8 mod[0x100];
u8 exp[0x04];
u8 padding1[0x34];
} __attribute__((packed)) Certificate;
typedef struct {
u32 commonkey_idx;
u8 reserved[4];
@ -71,6 +88,7 @@ typedef struct {
} __attribute__((packed)) TitleKeysInfo;
u32 ValidateTicket(Ticket* ticket);
u32 ValidateTicketSignature(Ticket* ticket);
u32 GetTitleKey(u8* titlekey, Ticket* ticket);
Ticket* TicketFromTickDbChunk(u8* chunk, u8* title_id, bool legit_pls);
u32 FindTicket(Ticket* ticket, u8* title_id, bool force_legit, bool emunand);

View File

@ -1784,7 +1784,7 @@ u32 BuildTitleKeyInfo(const char* path, bool dec, bool dump) {
return 1;
}
for (; data + TICKET_SIZE < ((u8*) TEMP_BUFFER) + read_bytes; data += 0x200) {
Ticket* ticket = TicketFromTickDbChunk(data, NULL, false);
Ticket* ticket = TicketFromTickDbChunk(data, NULL, true);
if (!ticket || (ticket->commonkey_idx >= 2) || !getbe64(ticket->ticket_id)) continue;
if (TIKDB_SIZE(tik_info) + 32 > MAIN_BUFFER_SIZE) return 1;
AddTicketToInfo(tik_info, ticket, dec); // ignore result
@ -1802,7 +1802,6 @@ u32 BuildTitleKeyInfo(const char* path, bool dec, bool dump) {
for (u32 i = 0; i < n_entries; i++, tik++) {
if (TIKDB_SIZE(tik_info) + 32 > MAIN_BUFFER_SIZE) return 1;
AddTitleKeyToInfo(tik_info, tik, !(filetype & FLAG_ENC), dec, false); // ignore result
}
}

View File

@ -80,7 +80,7 @@ u32 InitVTickDbDrive(void) { // prerequisite: ticket.db mounted as image
return 0;
}
for (; data + TICKET_SIZE < ((u8*) TEMP_BUFFER) + read_bytes; data += 0x200) {
Ticket* ticket = TicketFromTickDbChunk(data, NULL, false);
Ticket* ticket = TicketFromTickDbChunk(data, NULL, true);
if (!ticket) continue;
AddTickDbInfo(tick_info, ticket, offset_area + i + (data - ((u8*) TEMP_BUFFER)) + 0x18);
}