diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 0f9622a8ac..a478209f58 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,26 @@ constexpr int ErrorNotFound = static_cast(0x8095f004); constexpr int ErrorInvalidArg = static_cast(0x8095177a); constexpr int ErrorInvalidId = static_cast(0x8095f006); +constexpr std::int64_t TicksPerSecond = 1000000; +constexpr std::int64_t TicksPerMinute = 60 * TicksPerSecond; +constexpr std::int64_t TicksPerHour = 60 * TicksPerMinute; +constexpr std::int64_t TicksPerDay = 24 * TicksPerHour; +constexpr std::int64_t UnixEpochDays = 719162; + +std::int64_t DaysFromCivil(std::int64_t year, unsigned month, unsigned day) { + year -= month <= 2; + const std::int64_t era = (year >= 0 ? year : year - 399) / 400; + const auto yearOfEra = static_cast(year - era * 400); + const unsigned dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5 + day - 1; + const unsigned dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + return era * 146097 + static_cast(dayOfEra) - 719468 + UnixEpochDays; +} + +std::uint64_t TimeToTick(const mbedtls_x509_time& time) { + return static_cast(DaysFromCivil(time.year, time.mon, time.day)) * TicksPerDay + + time.hour * TicksPerHour + time.min * TicksPerMinute + time.sec * TicksPerSecond; +} + struct Pool { std::size_t size, current = 0, peak = 0; bool exhausted = false; @@ -518,9 +539,10 @@ int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) return 0; } -int APS5_VABI sceSslClose() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSslClose(int sslConnectionId) { + (void)sslConnectionId; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceSslGetSerialNumber(int sslCtxId, void* sslCert, unsigned char* sboData, std::size_t* sboLen) { @@ -625,6 +647,42 @@ int APS5_VABI sceSslGetNameEntryInfo(int sslCtxId, void* certName, int entryNum, return 0; } +int APS5_VABI sceSslGetNotBefore(int sslCtxId, void* sslCert, SceSsl::RtcTick* begin) { + if (!begin) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + begin->tick = TimeToTick(crt->valid_from); + return 0; +} + +int APS5_VABI sceSslGetNotAfter(int sslCtxId, void* sslCert, SceSsl::RtcTick* limit) { + if (!limit) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + limit->tick = TimeToTick(crt->valid_to); + return 0; +} + +int APS5_VABI sceSslGetFingerprint(int sslCtxId, void* sslCert, int hashType, unsigned char* fingerprint) { + if (!fingerprint) return ErrorInvalidArg; + mbedtls_md_type_t mdType; + switch (hashType) { + case 0: mdType = MBEDTLS_MD_MD5; break; + case 1: mdType = MBEDTLS_MD_SHA1; break; + case 2: mdType = MBEDTLS_MD_SHA224; break; + case 3: mdType = MBEDTLS_MD_SHA256; break; + case 4: mdType = MBEDTLS_MD_SHA384; break; + case 5: mdType = MBEDTLS_MD_SHA512; break; + default: return ErrorInvalidArg; + } + const auto* info = mbedtls_md_info_from_type(mdType); + if (!info) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + if (mbedtls_md(info, crt->raw.p, crt->raw.len, fingerprint) != 0) return ErrorInvalidArg; + return 0; +} + int APS5_VABI sceSslGetPem(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceSsl/SslTypes.hpp b/core/libs/prx/libSceSsl/SslTypes.hpp index 18fc70f740..f82ac92e0e 100644 --- a/core/libs/prx/libSceSsl/SslTypes.hpp +++ b/core/libs/prx/libSceSsl/SslTypes.hpp @@ -26,9 +26,14 @@ struct MemoryPoolStats { std::uint32_t reserved; }; +struct RtcTick { + std::uint64_t tick; +}; + static_assert(sizeof(Data) == 0x10); static_assert(sizeof(CaCerts) == 0x18); static_assert(sizeof(CaList) == 0x10); static_assert(sizeof(MemoryPoolStats) == 0x20); +static_assert(sizeof(RtcTick) == 0x8); } diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index 8c26ab31c7..4cb3a27c3e 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -29,6 +29,10 @@ 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 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*); } namespace { @@ -236,11 +240,54 @@ void CertificateReaders() { Throws([&] { sceSslFreeSslCertName(id, fake); }); Throws([&] { sceSslFreeSslCertName(9999, subject); }); + SceSsl::RtcTick begin{}; + Check(sceSslGetNotBefore(id, cert, &begin) == 0, "get not before failed"); + Check(begin.tick == 63927094442000000ULL, "not before tick mismatch"); + Check(sceSslGetNotBefore(id, cert, nullptr) == static_cast(0x8095177a), "null begin accepted"); + Throws([&] { sceSslGetNotBefore(id, nullptr, &begin); }); + Throws([&] { sceSslGetNotBefore(id, fake, &begin); }); + Throws([&] { sceSslGetNotBefore(9999, cert, &begin); }); + + SceSsl::RtcTick limit{}; + Check(sceSslGetNotAfter(id, cert, &limit) == 0, "get not after failed"); + Check(limit.tick == 64242454442000000ULL, "not after tick mismatch"); + Check(sceSslGetNotAfter(id, cert, nullptr) == static_cast(0x8095177a), "null limit accepted"); + Throws([&] { sceSslGetNotAfter(id, nullptr, &limit); }); + Throws([&] { sceSslGetNotAfter(id, fake, &limit); }); + Throws([&] { sceSslGetNotAfter(9999, cert, &limit); }); + + unsigned char md5[16]{}; + Check(sceSslGetFingerprint(id, cert, 0, md5) == 0, "get md5 fingerprint failed"); + constexpr unsigned char expectedMd5[16] = {0x71, 0x3c, 0xe5, 0x20, 0x88, 0x2b, 0x6b, 0xd0, 0x17, 0x85, 0x07, 0xde, 0xbd, 0xdc, 0xa9, 0x4f}; + Check(std::memcmp(md5, expectedMd5, sizeof(md5)) == 0, "md5 digest mismatch"); + + unsigned char sha1[20]{}; + Check(sceSslGetFingerprint(id, cert, 1, sha1) == 0, "get sha1 fingerprint failed"); + constexpr unsigned char expectedSha1[20] = {0x47, 0xe0, 0x89, 0xab, 0xb7, 0x02, 0xee, 0xb6, 0x2f, 0x8b, 0x5b, 0x7a, 0x13, 0xfe, 0x82, 0x6d, 0x2b, 0xe8, 0xee, 0xa7}; + Check(std::memcmp(sha1, expectedSha1, sizeof(sha1)) == 0, "sha1 digest mismatch"); + + unsigned char sha256[32]{}; + Check(sceSslGetFingerprint(id, cert, 3, sha256) == 0, "get sha256 fingerprint failed"); + constexpr unsigned char expectedSha256[32] = {0x3d, 0x7d, 0xac, 0xd3, 0x04, 0x90, 0x42, 0xe1, 0x57, 0x19, 0x87, 0x27, 0x54, 0x45, 0xae, 0xed, 0xc4, 0xea, 0xd6, 0xd4, 0x20, 0xa0, 0x7f, 0xc4, 0xf5, 0x26, 0xc1, 0xa0, 0x85, 0x87, 0x5a, 0x4c}; + Check(std::memcmp(sha256, expectedSha256, sizeof(sha256)) == 0, "sha256 digest mismatch"); + + Check(sceSslGetFingerprint(id, cert, 3, nullptr) == static_cast(0x8095177a), "null fingerprint buffer accepted"); + Check(sceSslGetFingerprint(id, cert, -1, sha256) == static_cast(0x8095177a), "negative hash type accepted"); + Check(sceSslGetFingerprint(id, cert, 99, sha256) == static_cast(0x8095177a), "invalid hash type accepted"); + Throws([&] { sceSslGetFingerprint(id, nullptr, 3, sha256); }); + Throws([&] { sceSslGetFingerprint(id, fake, 3, sha256); }); + Throws([&] { sceSslGetFingerprint(9999, cert, 3, sha256); }); + + Throws([&] { sceSslClose(1); }); + Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed"); Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); Throws([&] { sceSslGetSerialNumber(id, cert, serialData, &serialLen); }); Throws([&] { sceSslGetSubjectName(id, cert); }); Throws([&] { sceSslGetIssuerName(id, cert); }); + Throws([&] { sceSslGetNotBefore(id, cert, &begin); }); + Throws([&] { sceSslGetNotAfter(id, cert, &limit); }); + Throws([&] { sceSslGetFingerprint(id, cert, 3, sha256); }); const int id2 = sceSslInit_nid_postfix(1024 * 1024); Check(sceSslLoadCert(id2, 1, &pointer, nullptr, nullptr) == 0, "CA load failed"); @@ -254,6 +301,9 @@ void CertificateReaders() { Throws([&] { sceSslGetSerialNumber(id2, cert2, serialData, &serialLen); }); Throws([&] { sceSslGetSubjectName(id2, cert2); }); Throws([&] { sceSslGetIssuerName(id2, cert2); }); + Throws([&] { sceSslGetNotBefore(id2, cert2, &begin); }); + Throws([&] { sceSslGetNotAfter(id2, cert2, &limit); }); + Throws([&] { sceSslGetFingerprint(id2, cert2, 3, sha256); }); Check(sceSslTerm_nid_postfix(id2) == 0, "term failed"); Throws([&] { sceSslGetNameEntryCount(id2, subject2); }); Throws([&] { sceSslFreeSslCertName(id2, subject2); }); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index abc8332e1b..b763182dc6 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,8 +2,7 @@ ### Unknown function info -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts -- [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName](../../../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). 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 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 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 +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite, sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead`, `sceSslWrite` and `sceSslClose` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts +- [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 neither starts a TLS connection nor authenticates a peer - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by PPSA01317 and PPSA07411. 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, the minimum version and the ALPN protocol list are recorded on the connection; nothing reads them until `sceSslConnect` has a transport, and how the console applies them is unknown. The initial verify options are unknown and taken as 0. `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. `sceSslSetAlpn` stores the string up to its terminator and throws for a null list; whether the console expects a comma-separated list or the TLS wire format is unknown. `sceSslGetAlpnSelected` returns null, as no handshake completes. An unknown connection and a connection whose context was terminated throw, as for `sceSslSetVerifyCallback`