Merge pull request #6511 from JJenkx/fix/ssl-ps5-reader-context

feat(ssl): implement context-scoped PS5 certificate PEM reading
This commit is contained in:
Adrià authored and GitHub committed 2026-10-19 05:01:14 +02:00
commit ba63d412cd
5 files changed
+152 -3

No files matched your search

+6 -1
View File
@@ -85,9 +85,14 @@ if(BUILD_TESTING)
add_test(NAME guest_ssl_transport_${table} COMMAND guest_ssl_transport_tests ${table})
set_tests_properties(guest_ssl_transport_${table} PROPERTIES TIMEOUT 30)
endforeach()
add_test_executable(guest_ssl_ps5_readers_tests ${LIBS_INCLUDE_DIR}/tests/GuestSslPs5Readers.cpp)
target_include_directories(guest_ssl_ps5_readers_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_ssl_ps5_readers_tests PRIVATE libSceSsl libc)
configure_windows_unwind(guest_ssl_ps5_readers_tests)
add_test(NAME guest_ssl_ps5_readers COMMAND guest_ssl_ps5_readers_tests)
if(WIN32)
target_link_libraries(guest_ssl_transport_tests PRIVATE bcrypt ws2_32)
set_tests_properties(guest_ssl guest_ssl_load guest_ssl_transport_kernel guest_ssl_transport_net PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(guest_ssl guest_ssl_load guest_ssl_ps5_readers guest_ssl_transport_kernel guest_ssl_transport_net PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSsl>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNet>")
endif()
endif()
+22 -2
View File
@@ -11,12 +11,14 @@
#include "prx/libkernel/Socket/include/SocketPoll.hpp"
#include <mbedtls/asn1.h>
#include <mbedtls/base64.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/entropy.h>
#include <mbedtls/error.h>
#include <mbedtls/md.h>
#include <mbedtls/net_sockets.h>
#include <mbedtls/oid.h>
#include <mbedtls/pem.h>
#include <mbedtls/pk.h>
#include <mbedtls/platform.h>
#include <mbedtls/platform_util.h>
@@ -1053,8 +1055,26 @@ int APS5_VABI sceSslGetFingerprint(int sslCtxId, void* sslCert, int hashType, un
return 0;
}
int APS5_VABI sceSslGetPem(void) {
NotImplemented_nid_no_patch(__func__);
int APS5_VABI sceSslGetPem(int sslCtxId, void* sslCert, char* buf, std::size_t* bufLen) {
if (!bufLen) return ErrorInvalidArg;
std::lock_guard lock(State().mutex);
auto& context = GetContext(sslCtxId);
const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert);
PoolScope scope(context.pool);
std::size_t required = 0;
const auto encode = [&](unsigned char* output, std::size_t capacity, std::size_t* written) {
return mbedtls_pem_write_buffer("-----BEGIN CERTIFICATE-----\n", "-----END CERTIFICATE-----\n", crt->raw.p, crt->raw.len, output, capacity, written);
};
const int sizing = encode(nullptr, 0, &required);
if (sizing != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) CheckBackend(sizing, "SSL PEM sizing failed");
if (!buf) {
*bufLen = required;
return 0;
}
if (*bufLen < required) throw std::length_error("SSL PEM buffer is too small");
std::size_t written = 0;
CheckBackend(encode(reinterpret_cast<unsigned char*>(buf), required, &written), "SSL PEM encoding failed");
*bufLen = written;
return 0;
}
+20
View File
@@ -33,6 +33,7 @@ int APS5_VABI sceSslClose(int);
int APS5_VABI sceSslGetNotBefore(int, void*, SceSsl::RtcTick*);
int APS5_VABI sceSslGetNotAfter(int, void*, SceSsl::RtcTick*);
int APS5_VABI sceSslGetFingerprint(int, void*, int, unsigned char*);
int APS5_VABI sceSslGetPem(int, void*, char*, std::size_t*);
}
namespace {
@@ -278,6 +279,23 @@ void CertificateReaders() {
Throws([&] { sceSslGetFingerprint(id, fake, 3, sha256); });
Throws([&] { sceSslGetFingerprint(9999, cert, 3, sha256); });
std::size_t pemLen = 0;
Check(sceSslGetPem(id, cert, nullptr, &pemLen) == 0, "query pem size failed");
Check(pemLen > 0, "pem length is 0");
std::vector<char> pemBuf(pemLen);
std::size_t writtenLen = pemBuf.size();
Check(sceSslGetPem(id, cert, pemBuf.data(), &writtenLen) == 0, "write pem failed");
Check(writtenLen == pemLen, "written pem length mismatch");
Check(std::strstr(pemBuf.data(), "-----BEGIN CERTIFICATE-----\n") != nullptr, "pem header missing");
Check(std::strstr(pemBuf.data(), "-----END CERTIFICATE-----\n") != nullptr, "pem footer missing");
char smallPem[8]{};
std::size_t smallPemLen = sizeof(smallPem);
Throws([&] { sceSslGetPem(id, cert, smallPem, &smallPemLen); });
Check(sceSslGetPem(id, cert, pemBuf.data(), nullptr) == static_cast<int>(0x8095177a), "null pem len accepted");
Throws([&] { sceSslGetPem(id, nullptr, pemBuf.data(), &writtenLen); });
Throws([&] { sceSslGetPem(id, fake, pemBuf.data(), &writtenLen); });
Throws([&] { sceSslClose(1); });
Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed");
@@ -288,6 +306,7 @@ void CertificateReaders() {
Throws([&] { sceSslGetNotBefore(id, cert, &begin); });
Throws([&] { sceSslGetNotAfter(id, cert, &limit); });
Throws([&] { sceSslGetFingerprint(id, cert, 3, sha256); });
Throws([&] { sceSslGetPem(id, cert, pemBuf.data(), &writtenLen); });
const int id2 = sceSslInit_nid_postfix(1024 * 1024);
Check(sceSslLoadCert(id2, 1, &pointer, nullptr, nullptr) == 0, "CA load failed");
@@ -304,6 +323,7 @@ void CertificateReaders() {
Throws([&] { sceSslGetNotBefore(id2, cert2, &begin); });
Throws([&] { sceSslGetNotAfter(id2, cert2, &limit); });
Throws([&] { sceSslGetFingerprint(id2, cert2, 3, sha256); });
Throws([&] { sceSslGetPem(id2, cert2, pemBuf.data(), &writtenLen); });
Check(sceSslTerm_nid_postfix(id2) == 0, "term failed");
Throws([&] { sceSslGetNameEntryCount(id2, subject2); });
Throws([&] { sceSslFreeSslCertName(id2, subject2); });
+103
View File
@@ -0,0 +1,103 @@
#include "prx/libSceSsl/SslTypes.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "SslCertificateFixture.hpp"
#include <algorithm>
#include <array>
#include <cstdio>
#include <cstring>
#include <exception>
#include <stdexcept>
#include <vector>
extern "C" {
int APS5_VABI sceSslInit_nid_postfix(std::uint64_t);
int APS5_VABI sceSslTerm_nid_postfix(int);
int APS5_VABI sceSslLoadCert(int, int, SceSsl::Data**, SceSsl::Data*, SceSsl::Data*);
int APS5_VABI sceSslUnloadCert(int);
int APS5_VABI sceSslGetCaList(int, SceSsl::CaList*);
int APS5_VABI sceSslFreeCaList(int, SceSsl::CaList*);
int APS5_VABI sceSslGetSerialNumber(int, void*, unsigned char*, std::size_t*);
void* APS5_VABI sceSslGetSubjectName(int, void*);
void* APS5_VABI sceSslGetIssuerName(int, void*);
int APS5_VABI sceSslGetNameEntryCount(int, void*);
int APS5_VABI sceSslGetNameEntryInfo(int, void*, int, char*, std::size_t, char*, std::size_t, std::size_t*);
int APS5_VABI sceSslFreeSslCertName(int, void*);
int APS5_VABI sceSslGetPem(int, void*, char*, std::size_t*);
}
namespace {
void Check(bool value, const char* message) {
if (!value) throw std::runtime_error(message);
}
template <class Function>
void Throws(Function function) {
try { function(); } catch (const std::exception&) { return; }
throw std::runtime_error("unsupported reader arguments were accepted");
}
void Readers() {
const int context = sceSslInit_nid_postfix(1024 * 1024);
const int other = sceSslInit_nid_postfix(1024 * 1024);
SceSsl::Data data{reinterpret_cast<std::uint8_t*>(const_cast<char*>(RootPem)), std::strlen(RootPem)};
auto* pointer = &data;
Check(sceSslLoadCert(context, 1, &pointer, nullptr, nullptr) == 0, "certificate load failed");
SceSsl::CaList list{};
Check(sceSslGetCaList(context, &list) == 0 && list.num == 1, "certificate handle absent");
void* certificate = list.certs[0];
std::size_t length = SIZE_MAX;
Check(sceSslGetSerialNumber(context, certificate, nullptr, &length) == 0 && length == 3, "PS5 serial size query failed");
std::array<unsigned char, 5> serial;
serial.fill(0xa5);
Check(sceSslGetSerialNumber(context, certificate, serial.data(), &length) == 0 && length == 3, "PS5 serial copy failed");
Check(serial == std::array<unsigned char, 5>{0x00, 0x80, 0xa1, 0xa5, 0xa5}, "serial copy changed its bytes or guards");
serial.fill(0xa5);
length = 2;
Check(sceSslGetSerialNumber(context, certificate, serial.data(), &length) == static_cast<int>(0x8095177a) && length == 3, "short serial buffer did not report its required size");
Check(std::all_of(serial.begin(), serial.end(), [](auto byte) { return byte == 0xa5; }), "short serial buffer was modified");
Throws([&] { sceSslGetSerialNumber(other, certificate, nullptr, &length); });
auto* subject = sceSslGetSubjectName(context, certificate);
auto* issuer = sceSslGetIssuerName(context, certificate);
Check(subject && issuer && subject != issuer, "certificate name absent");
Check(sceSslGetNameEntryCount(context, subject) == 3 && sceSslGetNameEntryCount(context, issuer) == 3, "PS5 name entry count failed");
Throws([&] { sceSslGetSubjectName(other, certificate); });
Throws([&] { sceSslGetNameEntryCount(other, subject); });
Throws([&] { sceSslFreeSslCertName(other, subject); });
std::array<char, 16> oid{}, value{};
length = SIZE_MAX;
Check(sceSslGetNameEntryInfo(context, subject, 1, oid.data(), oid.size(), value.data(), value.size(), &length) == 0, "PS5 eight-argument name query failed");
Check(std::strcmp(oid.data(), "O") == 0 && std::strcmp(value.data(), "AnyPS5 Tests") == 0 && length == 12, "name output or full-width length changed");
length = SIZE_MAX;
Check(sceSslGetPem(context, certificate, nullptr, &length) == 0 && length == std::strlen(RootPem) + 1, "PS5 PEM size query failed");
std::vector<char> pem(length + 2, static_cast<char>(0x5a));
Check(sceSslGetPem(context, certificate, pem.data(), &length) == 0 && length == std::strlen(RootPem) + 1, "PS5 PEM copy failed");
Check(std::strcmp(pem.data(), RootPem) == 0 && pem[length] == 0x5a && pem[length + 1] == 0x5a, "PEM copy changed bytes or guards");
std::array<char, 4> small{'x', 'x', 'x', 'x'};
length = 2;
Throws([&] { sceSslGetPem(context, certificate, small.data(), &length); });
Check(std::all_of(small.begin(), small.end(), [](char byte) { return byte == 'x'; }), "short PEM buffer was modified");
Throws([&] { sceSslGetPem(other, certificate, nullptr, &length); });
Check(sceSslUnloadCert(context) == 0, "unload failed");
Check(sceSslGetSerialNumber(context, certificate, nullptr, &length) == 0 && length == 3, "CA snapshot lost its certificate on unload");
Check(sceSslFreeCaList(context, &list) == 0, "CA list free failed");
Throws([&] { sceSslGetSerialNumber(context, certificate, nullptr, &length); });
Check(sceSslGetNameEntryCount(context, subject) == 3, "owned name depended on freed certificate storage");
Check(sceSslFreeSslCertName(context, subject) == 0, "name free failed");
Throws([&] { sceSslGetNameEntryCount(context, subject); });
Check(sceSslTerm_nid_postfix(context) == 0, "context term failed");
Throws([&] { sceSslGetNameEntryCount(context, issuer); });
Check(sceSslTerm_nid_postfix(other) == 0, "other context term failed");
}
}
int main() {
try { Readers(); } catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}
+1
View File
@@ -7,3 +7,4 @@
- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings with the full value length in `valueLen`, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5
- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate
- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures. The verify options select which verification failures `sceSslConnect` reports (see the connection entry above), with the bit values SharpProspero's `SceSslVerifyOption` gives: `SERVER_VERIFY` 0x1, `CN_CHECK` 0x4, `NOT_AFTER_CHECK` 0x8, `NOT_BEFORE_CHECK` 0x10, `KNOWN_CA_CHECK` 0x20. The initial options are all five checks (0x3D), so a connection whose title never calls these fails closed; what the console starts with is unknown, and the one source, the HTTPS layer of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L191), starts its libSceHttp connections with `ssl_flags` 0xA7 (`SERVER_VERIFY`, `CLIENT_VERIFY`, `CN_CHECK`, `KNOWN_CA_CHECK`, SNI), evidence that verification is on by default rather than for these bits. A check that is disabled is not reported, whatever the chain. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values SharpProspero lists, 0 (none) to 5 (TLS 1.2), and throws for others; whether the console accepts a TLS 1.3 value is unknown. Every accepted value gives the same handshake, as Mbed TLS 3 speaks TLS 1.2 and nothing older and TLS 1.3 is compiled out. `sceSslSetAlpn` stores the string up to its terminator and throws for a null list; it is split on commas into the protocols offered in the handshake, as HTTP libraries write ALPN lists, and whether the console expects that or the TLS wire format is unknown. `sceSslGetAlpnSelected` returns the protocol the server selected, or null before the handshake and when none was selected; the pointer is owned by the connection. An unknown connection and a connection whose context was terminated throw, as for `sceSslSetVerifyCallback`
- [sceSslGetPem](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - static inspection of PPSA28002 confirms the context-first four-argument call with a caller-owned buffer and an in/out 64-bit length. Mbed TLS serializes PEM including its trailing NUL. Buffers smaller than the required encoding throw; an unknown context or foreign or expired certificate handle throws. The exact console PEM length policy, error precedence and truncation behavior have not been measured on a PS5. Existing serial/name, validity and fingerprint readers and their documented uncertainties remain unchanged