Skip to content

Commit 42ced3e

Browse files
committed
[Kernel] Replace BCrypt RSA with portable bignum implementation
Removes the Windows-only BCrypt dependency from XeCryptBnQwNeRsaPubCrypt and replaces it with a portable modular exponentiation implementation using 64-bit arithmetic, enabling RSA signature verification on all platforms. Adds Catch2 tests validating the implementation with a 2048-bit RSA key.
1 parent 32eee83 commit 42ced3e

File tree

5 files changed

+612
-100
lines changed

5 files changed

+612
-100
lines changed

src/xenia/kernel/premake5.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,7 @@ project("xenia-kernel")
5151
files({
5252
"debug_visualizers.natvis",
5353
})
54+
55+
if enableTests then
56+
include("testing")
57+
end
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#include "third_party/catch/include/catch.hpp"
2+
#include "xenia/kernel/xboxkrnl/xecrypt_rsa.h"
3+
4+
namespace {
5+
6+
// Helper to build an XECRYPT_RSA-compatible blob in memory.
7+
// Layout: [4 bytes size BE][4 bytes exponent BE][8 bytes pad][modulus limbs...]
8+
std::vector<uint8_t> build_rsa_blob(uint32_t num_qwords, uint32_t exponent,
9+
const uint8_t* modulus_limbs) {
10+
std::vector<uint8_t> blob(0x10 + num_qwords * 8);
11+
// size (big-endian)
12+
blob[0] = (num_qwords >> 24) & 0xFF;
13+
blob[1] = (num_qwords >> 16) & 0xFF;
14+
blob[2] = (num_qwords >> 8) & 0xFF;
15+
blob[3] = num_qwords & 0xFF;
16+
// exponent (big-endian)
17+
blob[4] = (exponent >> 24) & 0xFF;
18+
blob[5] = (exponent >> 16) & 0xFF;
19+
blob[6] = (exponent >> 8) & 0xFF;
20+
blob[7] = exponent & 0xFF;
21+
// pad = 0
22+
// modulus data
23+
std::memcpy(blob.data() + 0x10, modulus_limbs, num_qwords * 8);
24+
return blob;
25+
}
26+
27+
// Pirs public key modulus (32 qwords = 2048 bits), exponent = 3
28+
static const uint8_t pirs_modulus[] = {
29+
0xE6, 0x3B, 0x32, 0xB2, 0x8D, 0x9E, 0x9E, 0xE7, 0x9D, 0xFC, 0x5C, 0x72,
30+
0x41, 0x94, 0x58, 0x47, 0xDE, 0x0D, 0x18, 0x40, 0x72, 0xD6, 0xE3, 0x46,
31+
0x8E, 0xBA, 0x8E, 0xBC, 0x1A, 0x90, 0xAC, 0x20, 0xBA, 0x03, 0x85, 0xB5,
32+
0x1A, 0x3E, 0x25, 0xF9, 0xA6, 0x58, 0xEB, 0xB6, 0xA3, 0xC4, 0xA3, 0xEE,
33+
0xB2, 0xB0, 0xAE, 0x97, 0x69, 0xEB, 0xFE, 0x71, 0xFC, 0x02, 0xAB, 0x77,
34+
0xBA, 0xC8, 0xE6, 0x74, 0xE6, 0x7C, 0x63, 0x0E, 0xAF, 0x4C, 0xF7, 0xE7,
35+
0x11, 0x4A, 0x80, 0x24, 0x72, 0x05, 0x7A, 0x63, 0xD0, 0xF8, 0x91, 0x02,
36+
0xA6, 0xE7, 0x7D, 0x77, 0xC5, 0xA7, 0x9B, 0x08, 0x11, 0x2E, 0xA0, 0x64,
37+
0x45, 0x60, 0x46, 0xBC, 0x36, 0xE1, 0x17, 0x71, 0xBE, 0x66, 0x49, 0x2F,
38+
0xAE, 0x20, 0xA4, 0x76, 0x9C, 0x27, 0x51, 0xCF, 0x4B, 0x34, 0x7A, 0x35,
39+
0xBC, 0xA4, 0xAA, 0x1C, 0x47, 0x4B, 0xF4, 0x97, 0x22, 0x4E, 0x13, 0x24,
40+
0xD3, 0xC1, 0x57, 0xDF, 0x4D, 0x84, 0xB9, 0x18, 0x97, 0x99, 0xAC, 0x00,
41+
0xB3, 0x3D, 0x03, 0x25, 0x60, 0xC8, 0x7A, 0x59, 0xFE, 0x48, 0xFF, 0x28,
42+
0x3D, 0x10, 0xBB, 0x9E, 0x09, 0x06, 0x2A, 0x61, 0x20, 0x2C, 0xF8, 0x72,
43+
0xEB, 0x87, 0xE6, 0xD1, 0xFB, 0xB3, 0x66, 0xFC, 0x4A, 0x02, 0xAE, 0xD4,
44+
0xD8, 0x37, 0xCF, 0xA6, 0x32, 0x25, 0x79, 0x36, 0x0E, 0xF4, 0xED, 0x19,
45+
0xA2, 0x10, 0x27, 0x96, 0x2F, 0x9F, 0xA9, 0x3D, 0xA4, 0x37, 0x30, 0x11,
46+
0x51, 0x83, 0xBD, 0xF7, 0xC7, 0xE5, 0xCE, 0xAA, 0xEC, 0xDE, 0x48, 0xA0,
47+
0x84, 0xF7, 0xB0, 0xF6, 0x4B, 0x8E, 0xF0, 0x89, 0xBD, 0x47, 0x7C, 0x90,
48+
0xDD, 0x88, 0x12, 0x17, 0x40, 0xD2, 0x4E, 0xA6, 0xC6, 0x11, 0x04, 0x1B,
49+
0x57, 0xA8, 0x68, 0xB4, 0x61, 0xF4, 0x1B, 0xC6, 0x8B, 0xE8, 0xD9, 0x20,
50+
0xF2, 0x05, 0xE0, 0x70,
51+
};
52+
53+
} // namespace
54+
55+
TEST_CASE("XeCryptBnQwNeRsaPubCrypt_rejects_small_keys", "[crypt]") {
56+
// Keys below 512 bits (8 qwords) must be rejected
57+
const uint8_t modulus[] = {
58+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
59+
};
60+
const uint8_t input[] = {
61+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
62+
};
63+
uint8_t output[8] = {};
64+
65+
REQUIRE(XeCryptBnQwNeRsaPubCrypt(input, output, modulus, 1, 3) == 0);
66+
67+
const uint8_t modulus2[] = {
68+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
69+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11,
70+
};
71+
const uint8_t input2[] = {
72+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
73+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
74+
};
75+
uint8_t output2[16] = {};
76+
77+
REQUIRE(XeCryptBnQwNeRsaPubCrypt(input2, output2, modulus2, 2, 65537) == 0);
78+
}
79+
80+
TEST_CASE("XeCryptBnQwNeRsaPubCrypt_2048bit_identity", "[crypt]") {
81+
// 1^3 mod modulus = 1
82+
uint8_t input[256] = {};
83+
input[7] = 0x01;
84+
85+
uint8_t output[256] = {};
86+
uint32_t ret = XeCryptBnQwNeRsaPubCrypt(input, output, pirs_modulus, 0x20, 3);
87+
88+
REQUIRE(ret == 1);
89+
uint8_t expected[256] = {};
90+
expected[7] = 0x01;
91+
REQUIRE(std::memcmp(output, expected, 256) == 0);
92+
}
93+
94+
TEST_CASE("XeCryptBnQwNeRsaPubCrypt_2048bit_small_base", "[crypt]") {
95+
// 2^3 = 8 < modulus, so result is 8
96+
uint8_t input[256] = {};
97+
input[7] = 0x02;
98+
99+
uint8_t output[256] = {};
100+
uint32_t ret = XeCryptBnQwNeRsaPubCrypt(input, output, pirs_modulus, 0x20, 3);
101+
102+
REQUIRE(ret == 1);
103+
uint8_t expected[256] = {};
104+
expected[7] = 0x08;
105+
REQUIRE(std::memcmp(output, expected, 256) == 0);
106+
}
107+
108+
TEST_CASE("XeCryptBnQwNeRsaPubCrypt_2048bit_large_base", "[crypt]") {
109+
// Large base that exercises modular reduction
110+
uint8_t input[256];
111+
for (int i = 0; i < 256; i++) {
112+
input[i] = static_cast<uint8_t>(i + 1);
113+
}
114+
// Ensure top limb is smaller than modulus top limb (0xF2...)
115+
input[248] = 0x01;
116+
input[249] = 0x02;
117+
input[250] = 0x03;
118+
input[251] = 0x04;
119+
input[252] = 0x05;
120+
input[253] = 0x06;
121+
input[254] = 0x07;
122+
input[255] = 0x08;
123+
124+
uint8_t output[256] = {};
125+
uint32_t ret = XeCryptBnQwNeRsaPubCrypt(input, output, pirs_modulus, 0x20, 3);
126+
REQUIRE(ret == 1);
127+
128+
// Deterministic: same input produces same output
129+
uint8_t output2[256] = {};
130+
uint32_t ret2 =
131+
XeCryptBnQwNeRsaPubCrypt(input, output2, pirs_modulus, 0x20, 3);
132+
REQUIRE(ret2 == 1);
133+
REQUIRE(std::memcmp(output, output2, 256) == 0);
134+
135+
// Non-trivial result
136+
bool all_zero = true;
137+
for (int i = 0; i < 256; i++) {
138+
if (output[i] != 0) {
139+
all_zero = false;
140+
break;
141+
}
142+
}
143+
REQUIRE_FALSE(all_zero);
144+
}
145+
146+
TEST_CASE("XeCryptBnQwNeRsaPubCrypt_2048bit_zero_base", "[crypt]") {
147+
// 0^e mod m = 0
148+
uint8_t input[256] = {};
149+
uint8_t output[256];
150+
std::memset(output, 0xFF, 256);
151+
152+
uint32_t ret =
153+
XeCryptBnQwNeRsaPubCrypt(input, output, pirs_modulus, 0x20, 65537);
154+
155+
REQUIRE(ret == 1);
156+
for (int i = 0; i < 256; i++) {
157+
REQUIRE(output[i] == 0x00);
158+
}
159+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
project_root = "../../../.."
2+
include(project_root.."/tools/build")
3+
4+
test_suite("xenia-kernel-tests", project_root, ".", {
5+
links = {
6+
"fmt",
7+
"xenia-base",
8+
"xenia-kernel",
9+
},
10+
})

src/xenia/kernel/xboxkrnl/xboxkrnl_crypt.cc

Lines changed: 9 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,14 @@
1616
#include "xenia/kernel/xboxkrnl/xboxkrnl_private.h"
1717
#include "xenia/xbox.h"
1818

19-
#ifdef XE_PLATFORM_WIN32
20-
#include "xenia/base/platform_win.h" // for bcrypt.h
21-
#endif
22-
2319
#include "third_party/crypto/TinySHA1.hpp"
2420
#include "third_party/crypto/des/des.cpp"
2521
#include "third_party/crypto/des/des.h"
2622
#include "third_party/crypto/des/des3.h"
2723
#include "third_party/crypto/des/descbc.h"
2824
#include "third_party/crypto/sha256.cpp"
2925
#include "third_party/crypto/sha256.h"
26+
#include "xenia/kernel/xboxkrnl/xecrypt_rsa.h"
3027

3128
extern "C" {
3229
#include "third_party/FFmpeg/libavutil/md5.h"
@@ -417,105 +414,17 @@ static_assert_size(XECRYPT_RSA, 0x10);
417414
dword_result_t XeCryptBnQwNeRsaPubCrypt_entry(pointer_t<uint64_t> qw_a,
418415
pointer_t<uint64_t> qw_b,
419416
pointer_t<XECRYPT_RSA> rsa) {
420-
// 0 indicates failure (but not a BOOL return value)
421-
#ifndef XE_PLATFORM_WIN32
422-
XELOGE(
423-
"XeCryptBnQwNeRsaPubCrypt called but no implementation available for "
424-
"this platform!");
425-
assert_always();
426-
return 1;
427-
#else
428-
uint32_t modulus_size = rsa->size * 8;
429-
430-
// Convert XECRYPT blob into BCrypt format
431-
ULONG key_size = sizeof(BCRYPT_RSAKEY_BLOB) + sizeof(uint32_t) + modulus_size;
432-
auto key_buf = std::make_unique<uint8_t[]>(key_size);
433-
auto* key_header = reinterpret_cast<BCRYPT_RSAKEY_BLOB*>(key_buf.get());
434-
435-
key_header->Magic = BCRYPT_RSAPUBLIC_MAGIC;
436-
key_header->BitLength = modulus_size * 8;
437-
key_header->cbPublicExp = sizeof(uint32_t);
438-
key_header->cbModulus = modulus_size;
439-
key_header->cbPrime1 = key_header->cbPrime2 = 0;
440-
441-
// Copy in exponent/modulus, luckily these are BE inside BCrypt blob
442-
uint32_t* key_exponent = reinterpret_cast<uint32_t*>(&key_header[1]);
443-
*key_exponent = rsa->public_exponent.value;
444-
445-
// ...except modulus needs to be reversed in 64-bit chunks for BCrypt to make
446-
// use of it properly for some reason
447-
uint64_t* key_modulus = reinterpret_cast<uint64_t*>(&key_exponent[1]);
448-
uint64_t* xecrypt_modulus = reinterpret_cast<uint64_t*>(&rsa[1]);
449-
std::reverse_copy(xecrypt_modulus, xecrypt_modulus + rsa->size, key_modulus);
450-
451-
BCRYPT_ALG_HANDLE hAlgorithm = NULL;
452-
NTSTATUS status = BCryptOpenAlgorithmProvider(
453-
&hAlgorithm, BCRYPT_RSA_ALGORITHM, MS_PRIMITIVE_PROVIDER, 0);
454-
455-
if (!BCRYPT_SUCCESS(status)) {
456-
XELOGE(
457-
"XeCryptBnQwNeRsaPubCrypt: BCryptOpenAlgorithmProvider failed with "
458-
"status {:#X}!",
459-
status);
460-
return 0;
461-
}
462-
463-
BCRYPT_KEY_HANDLE hKey = NULL;
464-
status = BCryptImportKeyPair(hAlgorithm, NULL, BCRYPT_RSAPUBLIC_BLOB, &hKey,
465-
key_buf.get(), key_size, 0);
466-
467-
if (!BCRYPT_SUCCESS(status)) {
468-
XELOGE(
469-
"XeCryptBnQwNeRsaPubCrypt: BCryptImportKeyPair failed with status "
470-
"{:#X}!",
471-
status);
472-
473-
if (hAlgorithm) {
474-
BCryptCloseAlgorithmProvider(hAlgorithm, 0);
475-
}
476-
477-
return 0;
478-
}
479-
480-
// Byteswap & reverse the input into output, as BCrypt wants MSB first
481-
uint64_t* output = qw_b;
482-
uint8_t* output_bytes = reinterpret_cast<uint8_t*>(output);
483-
xe::copy_and_swap<uint64_t>(output, qw_a, rsa->size);
484-
std::reverse(output_bytes, output_bytes + modulus_size);
485-
486-
// BCryptDecrypt only works with private keys, fortunately BCryptEncrypt
487-
// performs the right actions needed for us to decrypt the input
488-
ULONG result_size = 0;
489-
status =
490-
BCryptEncrypt(hKey, output_bytes, modulus_size, nullptr, nullptr, 0,
491-
output_bytes, modulus_size, &result_size, BCRYPT_PAD_NONE);
492-
493-
assert(result_size == modulus_size);
494-
495-
if (!BCRYPT_SUCCESS(status)) {
496-
XELOGE("XeCryptBnQwNeRsaPubCrypt: BCryptEncrypt failed with status {:#X}!",
497-
status);
498-
} else {
499-
// Reverse data & byteswap again so data is as game expects
500-
std::reverse(output_bytes, output_bytes + modulus_size);
501-
xe::copy_and_swap(output, output, rsa->size);
502-
}
503-
504-
if (hKey) {
505-
BCryptDestroyKey(hKey);
506-
}
507-
if (hAlgorithm) {
508-
BCryptCloseAlgorithmProvider(hAlgorithm, 0);
509-
}
417+
uint32_t num_qwords = rsa->size;
418+
uint32_t exponent = rsa->public_exponent;
419+
const uint8_t* input_bytes = reinterpret_cast<const uint8_t*>(&qw_a[0]);
420+
uint8_t* output_bytes = reinterpret_cast<uint8_t*>(&qw_b[0]);
421+
const uint8_t* mod_bytes =
422+
reinterpret_cast<const uint8_t*>(&rsa[1]); // modulus follows header
510423

511-
return BCRYPT_SUCCESS(status) ? 1 : 0;
512-
#endif
424+
return XeCryptBnQwNeRsaPubCrypt(input_bytes, output_bytes, mod_bytes,
425+
num_qwords, exponent);
513426
}
514-
#ifdef XE_PLATFORM_WIN32
515427
DECLARE_XBOXKRNL_EXPORT1(XeCryptBnQwNeRsaPubCrypt, kNone, kImplemented);
516-
#else
517-
DECLARE_XBOXKRNL_EXPORT1(XeCryptBnQwNeRsaPubCrypt, kNone, kStub);
518-
#endif
519428

520429
dword_result_t XeCryptBnDwLePkcs1Verify_entry(lpvoid_t hash, lpvoid_t sig,
521430
dword_t size) {

0 commit comments

Comments
 (0)