KIRK: Add ECDSA tests and a benchmark

Command 1's ECDSA check, behind every ECDSA-signed (type 6) module, takes
55ms in a Release build, and booting the XMB from a 6.60 firmware does it
40 times. The vectors are signatures from those modules.

kirk1_ecdsa_verify() factors the check out of kirk_CMD1 so the test can
call it without building a whole command 1 block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-11-07 08:50:04 +01:00
1 parent d4e3c7eb2b
commit 413bb4da97
8 files changed
+220 -11

No files matched your search

+1
View File
@@ -1375,6 +1375,7 @@ if(UNITTEST)
unittest/TestZipSlip.cpp
unittest/TestPS1Eboot.cpp
unittest/TestLzrc.cpp
unittest/TestKirk.cpp
unittest/TestCwCheat.cpp
unittest/TestMpegCsc.cpp
unittest/TestSplineTessellation.cpp
+1
View File
@@ -1200,6 +1200,7 @@ ifeq ($(UNITTEST),1)
$(SRC)/unittest/TestVFS.cpp \
$(SRC)/unittest/TestDemangle.cpp \
$(SRC)/unittest/TestLzrc.cpp \
$(SRC)/unittest/TestKirk.cpp \
$(SRC)/unittest/TestCwCheat.cpp \
$(SRC)/unittest/TestMpegCsc.cpp \
$(SRC)/unittest/TestSplineTessellation.cpp \
+16 -11
View File
@@ -253,6 +253,16 @@ int kirk_CMD0(KirkState *kirk, u8* outbuff, const u8* inbuff, int size, int gene
return KIRK_OPERATION_SUCCESS;
}
int kirk1_ecdsa_verify(const u8 *hash, const u8 *r, const u8 *s)
{
u8 kirk1_pub[40];
ecdsa_set_curve(ec_p,ec_a,ec_b1,ec_N1,Gx1,Gy1);
memcpy(kirk1_pub,Px1,20);
memcpy(kirk1_pub+20,Py1,20);
ecdsa_set_pub(kirk1_pub);
return ecdsa_verify((u8 *)hash, (u8 *)r, (u8 *)s);
}
// This one writes to inbuff.
int kirk_CMD1(KirkState *kirk, u8* outbuff, u8* inbuff, int size)
{
@@ -270,26 +280,21 @@ int kirk_CMD1(KirkState *kirk, u8* outbuff, u8* inbuff, int size)
{
SHA_CTX sha;
KIRK_CMD1_ECDSA_HEADER* eheader = (KIRK_CMD1_ECDSA_HEADER*) inbuff;
u8 kirk1_pub[40];
u8 header_hash[20];
u8 data_hash[20];
ecdsa_set_curve(ec_p,ec_a,ec_b1,ec_N1,Gx1,Gy1);
memcpy(kirk1_pub,Px1,20);
memcpy(kirk1_pub+20,Py1,20);
ecdsa_set_pub(kirk1_pub);
//Hash the Header
SHAInit(&sha);
SHAUpdate(&sha, (u8*)eheader+0x60, 0x30);
SHAFinal(header_hash, &sha);
if(!ecdsa_verify(header_hash,eheader->header_sig_r,eheader->header_sig_s)) {
SHAFinal(header_hash, &sha);
if(!kirk1_ecdsa_verify(header_hash,eheader->header_sig_r,eheader->header_sig_s)) {
return KIRK_HEADER_HASH_INVALID;
}
SHAInit(&sha);
SHAUpdate(&sha, (u8*)eheader+0x60, size-0x60);
SHAFinal(data_hash, &sha);
if(!ecdsa_verify(data_hash,eheader->data_sig_r,eheader->data_sig_s)) {
SHAFinal(data_hash, &sha);
if(!kirk1_ecdsa_verify(data_hash,eheader->data_sig_r,eheader->data_sig_s)) {
return KIRK_DATA_HASH_INVALID;
}
+2
View File
@@ -227,6 +227,8 @@ int kirk_CMD13(KirkState *kirk, u8* outbuff, int outsize,u8* inbuff, int insize)
int kirk_CMD14(KirkState *kirk, u8* outbuff, int outsize);
int kirk_CMD16(KirkState *kirk, u8* outbuff, int outsize,u8* inbuff, int insize);
int kirk_CMD17(KirkState *kirk, const u8* inbuff, int insize);
// KIRK command 1's ECDSA check, against the fixed public key on its own curve.
int kirk1_ecdsa_verify(const u8 *hash, const u8 *r, const u8 *s);
int kirk_init(KirkState *kirk); //CMD 0xF?
int kirk_init2(KirkState *kirk, u8 *, u32, u32, u32);
+194
View File
@@ -0,0 +1,194 @@
// Copyright (c) 2012- PPSSPP Project.
// 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, version 2.0 or later versions.
// 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 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <cstdio>
#include <cstring>
#include "Common/Common.h"
#include "Common/CommonTypes.h"
#include "ext/libkirk/kirk_engine.h"
#include "UnitTest.h"
static void FromHex(u8 *out, const char *hex, size_t len) {
for (size_t i = 0; i < len; i++) {
unsigned v = 0;
sscanf(hex + i * 2, "%2x", &v);
out[i] = (u8)v;
}
}
static bool EqualHex(const u8 *data, const char *hex, size_t len) {
u8 expected[64];
FromHex(expected, hex, len);
if (memcmp(data, expected, len) == 0) {
return true;
}
printf("got ");
for (size_t i = 0; i < len; i++) {
printf("%02x", data[i]);
}
printf("\nexpected %s\n", hex);
return false;
}
// KIRK command 1 signatures from the 6.60 firmware's ECDSA-signed (type 6) modules, as the XMB
// checks them while booting: hash, R, S.
static const char *const kirk1Vectors[][3] = {
{ "a269699e067b96da49d66b7b2d70f7ffbebcdcbf", "664fe1f2e9d66336f7330bcab9556db6ebe805dc", "b93605ea2a5ac814ad14b153985359b160aa084c" },
{ "1b73ae788bb2e018ba942bd12a29d46f78aab4a6", "cde388a63c1d57dc5e94eeac2e6c9f2e81c71c58", "fdbf626af2944be6deaab6d8c106ec066d812156" },
{ "fc38663691bc1eefbbf63410a79ec657013711ee", "664fe1f2e9d66336f7330bcab9556db6ebe805dc", "102f5e879e2ad91bc9fb74d446f751fcd6569d0b" },
{ "daa435a2eca3a3f5a0937cfebee52c14742a0727", "cde388a63c1d57dc5e94eeac2e6c9f2e81c71c58", "133c774b49bd2e8afc9979b089ce74c839f5bb72" },
{ "267ae5575cf2e66619d0e06a24ca58e4076d6c78", "664fe1f2e9d66336f7330bcab9556db6ebe805dc", "5a07c4ce9127f3e6b7cfc7b420da4ac8e5874ede" },
{ "12ffabc1a9e625f0b1d5c3c5e5d3cc10135cbf4c", "cde388a63c1d57dc5e94eeac2e6c9f2e81c71c58", "f162b40d5e2b7b4f6cbffd8ee2a9c57b42bdaf70" },
};
// The base point of the curve commands 0xC-0x11 use.
static const char *const curve2G = "128ec4256487fd8fdf64e2437bc0a1f6d5afde2c" "5958557eb1db001260425524dbc379d5ac5f4adf";
static const char *const pointMulScalar = "0123456789abcdeffedcba98765432100f1e2d3c";
static const char *const pointMulResult = "e22f6d6a26da003b886b2af8b720287322964ba8" "4830b64e45a95ed43664e434d8aae6e99dc36280";
// A signature made with command 0x10: public key, hash, R, S.
static const char *const cmd17Vector[4] = {
"3663f4b14711f0494fa0fe1c3dff7b15e9cbc189" "df3a94bd2ccc095b0b386f65aa5a5a128b502125",
"404346494c4f5255585b5e6164676a6d70737679",
"da6c3bc1bbf907d7f3ad4cbf14a164e326bb2273",
"641bdb12516febd68f9ccc2051912c6fa9495a40",
};
static bool Kirk1Verify(int i, int flipByte) {
u8 hash[20], r[20], s[20];
FromHex(hash, kirk1Vectors[i][0], 20);
FromHex(r, kirk1Vectors[i][1], 20);
FromHex(s, kirk1Vectors[i][2], 20);
if (flipByte >= 0) {
u8 *all[3] = { hash, r, s };
all[flipByte / 20][flipByte % 20] ^= 0x10;
}
return kirk1_ecdsa_verify(hash, r, s) != 0;
}
static int PointMul(KirkState *kirk, u8 out[0x28], const u8 scalar[0x14], const u8 point[0x28]) {
KIRK_CMD13_BUFFER in;
memcpy(in.multiplier, scalar, 0x14);
memcpy(in.public_key.x, point, 0x28);
return kirk_sceUtilsBufferCopyWithRange(kirk, out, 0x28, (u8 *)&in, sizeof(in), KIRK_CMD_ECDSA_MULTIPLY_POINT);
}
static int Sign(KirkState *kirk, ECDSA_SIG *sig, const u8 privateKey[0x14], const u8 hash[0x14]) {
u8 dec[0x20]{};
memcpy(dec, privateKey, 0x14);
KIRK_CMD16_BUFFER in;
encrypt_kirk16_private(kirk, in.enc_private, dec);
memcpy(in.message_hash, hash, 0x14);
return kirk_sceUtilsBufferCopyWithRange(kirk, (u8 *)sig, sizeof(*sig), (u8 *)&in, sizeof(in), KIRK_CMD_ECDSA_SIGN);
}
static int Verify(KirkState *kirk, const KIRK_CMD17_BUFFER &buf) {
return kirk_sceUtilsBufferCopyWithRange(kirk, nullptr, 0, (u8 *)&buf, sizeof(buf), KIRK_CMD_ECDSA_VERIFY);
}
bool TestKirkEcdsa() {
KirkState kirk{};
kirk_init(&kirk);
const int numVectors = (int)ARRAY_SIZE(kirk1Vectors);
for (int i = 0; i < numVectors; i++) {
EXPECT_TRUE(Kirk1Verify(i, -1));
// A bit off in the hash, R or S.
EXPECT_FALSE(Kirk1Verify(i, 7));
EXPECT_FALSE(Kirk1Verify(i, 20 + 19));
EXPECT_FALSE(Kirk1Verify(i, 40));
}
u8 g[0x28], scalar[0x14], out[0x28];
FromHex(g, curve2G, 0x28);
FromHex(scalar, pointMulScalar, 0x14);
EXPECT_EQ_INT(PointMul(&kirk, out, scalar, g), 0);
EXPECT_TRUE(EqualHex(out, pointMulResult, 0x28));
KIRK_CMD17_BUFFER pinned;
FromHex(pinned.public_key.x, cmd17Vector[0], 0x28);
FromHex(pinned.message_hash, cmd17Vector[1], 0x14);
FromHex(pinned.signature.r, cmd17Vector[2], 0x14);
FromHex(pinned.signature.s, cmd17Vector[3], 0x14);
EXPECT_EQ_INT(Verify(&kirk, pinned), 0);
pinned.signature.s[3] ^= 1;
EXPECT_EQ_INT(Verify(&kirk, pinned), KIRK_SIG_CHECK_INVALID);
// Fresh keys and signatures (the PRNG mixes in the time): the public key command 0xC returns has
// to be its private key times G, and what command 0x10 signs has to pass command 0x11.
for (int i = 0; i < 4; i++) {
KIRK_CMD12_BUFFER keys;
EXPECT_EQ_INT(kirk_sceUtilsBufferCopyWithRange(&kirk, (u8 *)&keys, sizeof(keys), nullptr, 0, KIRK_CMD_ECDSA_GEN_KEYS), 0);
EXPECT_EQ_INT(PointMul(&kirk, out, keys.private_key, g), 0);
EXPECT_TRUE(memcmp(out, keys.public_key.x, 0x28) == 0);
KIRK_CMD17_BUFFER buf;
memcpy(&buf.public_key, &keys.public_key, sizeof(buf.public_key));
for (int j = 0; j < 0x14; j++) {
buf.message_hash[j] = (u8)(i * 31 + j * 7);
}
EXPECT_EQ_INT(Sign(&kirk, &buf.signature, keys.private_key, buf.message_hash), 0);
EXPECT_EQ_INT(Verify(&kirk, buf), 0);
buf.message_hash[0] ^= 0x80;
EXPECT_EQ_INT(Verify(&kirk, buf), KIRK_SIG_CHECK_INVALID);
}
return true;
}
// Microseconds per KIRK ECDSA operation. Command 1's check runs twice per ECDSA-signed module the
// firmware loads.
bool TestKirkEcdsaBench() {
KirkState kirk{};
kirk_init(&kirk);
int sink = 0;
int which = 0;
const double kirk1 = CallsPerSecond([&] {
sink += Kirk1Verify(which++ % ARRAY_SIZE(kirk1Vectors), -1);
}, 0.25, 1);
u8 g[0x28], scalar[0x14], out[0x28];
FromHex(g, curve2G, 0x28);
FromHex(scalar, pointMulScalar, 0x14);
const double pointMul = CallsPerSecond([&] {
sink += PointMul(&kirk, out, scalar, g);
}, 0.25, 1);
KIRK_CMD17_BUFFER buf;
FromHex(buf.public_key.x, cmd17Vector[0], 0x28);
FromHex(buf.message_hash, cmd17Vector[1], 0x14);
FromHex(buf.signature.r, cmd17Vector[2], 0x14);
FromHex(buf.signature.s, cmd17Vector[3], 0x14);
const double verify = CallsPerSecond([&] {
sink += Verify(&kirk, buf);
}, 0.25, 1);
KIRK_CMD12_BUFFER keys;
kirk_sceUtilsBufferCopyWithRange(&kirk, (u8 *)&keys, sizeof(keys), nullptr, 0, KIRK_CMD_ECDSA_GEN_KEYS);
ECDSA_SIG sig;
const double sign = CallsPerSecond([&] {
sink += Sign(&kirk, &sig, keys.private_key, buf.message_hash);
}, 0.25, 1);
printf("KIRK 1 ECDSA verify: %10.1f us\n", 1e6 / kirk1);
printf("KIRK 0xD point multiply: %10.1f us\n", 1e6 / pointMul);
printf("KIRK 0x10 sign: %10.1f us\n", 1e6 / sign);
printf("KIRK 0x11 verify: %10.1f us\n", 1e6 / verify);
return sink != 12345;
}
+4
View File
@@ -4203,6 +4203,8 @@ bool TestVFS();
bool TestZipSlip();
bool TestPS1Eboot();
bool TestLzrc();
bool TestKirkEcdsa();
bool TestKirkEcdsaBench();
bool TestCwCheat();
bool TestMpegCsc();
bool TestSplineTessellation();
@@ -4761,6 +4763,8 @@ TestItem availableTests[] = {
TEST_ITEM(ZipSlip),
TEST_ITEM(PS1Eboot),
TEST_ITEM(Lzrc),
TEST_ITEM(KirkEcdsa),
TEST_ITEM(KirkEcdsaBench),
TEST_ITEM(CwCheat),
TEST_ITEM(MpegCsc),
TEST_ITEM(SplineTessellation),
+1
View File
@@ -289,6 +289,7 @@
<ClCompile Include="TestShaderGenerators.cpp" />
<ClCompile Include="TestDemangle.cpp" />
<ClCompile Include="TestLzrc.cpp" />
<ClCompile Include="TestKirk.cpp" />
<ClCompile Include="TestCwCheat.cpp" />
<ClCompile Include="TestMpegCsc.cpp" />
<ClCompile Include="TestSplineTessellation.cpp" />
+1
View File
@@ -21,6 +21,7 @@
<ClCompile Include="TestIRPassSimplify.cpp" />
<ClCompile Include="TestDemangle.cpp" />
<ClCompile Include="TestLzrc.cpp" />
<ClCompile Include="TestKirk.cpp" />
<ClCompile Include="TestCwCheat.cpp" />
<ClCompile Include="TestMpegCsc.cpp" />
<ClCompile Include="TestSplineTessellation.cpp" />