diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 5d7394fdb3..636719b717 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1,7 +1,6 @@ #ifdef _WIN32 #include #else -#include #include #include #endif @@ -52,15 +51,21 @@ constexpr int ErrorFatalAlert = static_cast(0x8095f010); constexpr int ErrorWantPollIn = static_cast(0x8095f013); constexpr int ErrorWantPollOut = static_cast(0x8095f014); constexpr int ErrorSocketClosed = static_cast(0x8095170d); -constexpr int ErrorReadTimeout = static_cast(0x80951710); constexpr int ErrorCallbackCheck = static_cast(0x8095f0fd); -constexpr int ReadPartial = 0x1; +constexpr int ErrorInvalidValue = static_cast(0x8095f007); +constexpr int ErrorBadLength = static_cast(0x80951773); +constexpr int ErrorBadRecord = static_cast(0x80951d55); +constexpr int ErrorNetBadDescriptor = static_cast(0x80410109); +constexpr int ErrorNetPipe = static_cast(0x80410120); +constexpr int ErrorNetReset = static_cast(0x80410136); +constexpr int ErrorKernelInvalid = static_cast(0x80020016); constexpr std::uint32_t FlagServerVerify = 0x1; constexpr std::uint32_t FlagCnCheck = 0x4; constexpr std::uint32_t FlagNotAfterCheck = 0x8; constexpr std::uint32_t FlagNotBeforeCheck = 0x10; constexpr std::uint32_t FlagKnownCaCheck = 0x20; -constexpr std::uint32_t InitialVerifyOptions = FlagServerVerify | FlagCnCheck | FlagNotAfterCheck | FlagNotBeforeCheck | FlagKnownCaCheck; +constexpr std::uint32_t VerifyOptionMask = 0x3f; +constexpr std::uint32_t InitialVerifyOptions = FlagServerVerify | FlagCnCheck | FlagKnownCaCheck; constexpr std::int64_t TicksPerSecond = 1000000; constexpr std::int64_t TicksPerMinute = 60 * TicksPerSecond; @@ -263,18 +268,19 @@ bool HostReset(int error) { #endif } -bool HostTimedOut(std::uintptr_t socket, int error) { +bool HostTimedOut(int error) { #ifdef _WIN32 - (void)socket; return error == WSAETIMEDOUT; #else - if (!HostWouldBlock(error)) return false; - const int flags = ::fcntl(static_cast(socket), F_GETFL); - return flags >= 0 && !(flags & O_NONBLOCK); + (void)error; + return false; #endif } +enum class Operation { Connect, Read, Write, Close }; + struct Session { + int contextHandle; std::shared_ptr context; std::shared_ptr identity; std::shared_ptr peer; @@ -291,15 +297,17 @@ struct Session { bool verifying = false; bool verified = false; bool closed = false; - bool timedOut = false; + bool reset = false; + bool gone = false; int verifyResult = 0; + unsigned int failedOptions = 0; Random random; mbedtls_x509_crt authorities; mbedtls_ssl_config config; mbedtls_ssl_context ssl; - Session(std::shared_ptr owner, std::shared_ptr socketHolder, std::uintptr_t native) - : context(std::move(owner)), identity(context->identity), socketOwner(std::move(socketHolder)), socket(native) { + Session(int handle, std::shared_ptr owner, std::shared_ptr socketHolder, std::uintptr_t native) + : contextHandle(handle), context(std::move(owner)), identity(context->identity), socketOwner(std::move(socketHolder)), socket(native) { mbedtls_x509_crt_init(&authorities); mbedtls_ssl_config_init(&config); mbedtls_ssl_init(&ssl); @@ -346,7 +354,6 @@ struct Session { static int Send(void* argument, const unsigned char* buffer, std::size_t length) { auto& session = *static_cast(argument); const auto chunk = static_cast(std::min(length, INT_MAX)); - session.timedOut = false; for (;;) { #ifdef _WIN32 const auto sent = ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); @@ -356,8 +363,7 @@ struct Session { if (sent >= 0) return static_cast(sent); const int error = HostError(); if (HostInterrupted(error)) continue; - session.timedOut = HostTimedOut(session.socket, error); - if (session.timedOut || HostWouldBlock(error)) return MBEDTLS_ERR_SSL_WANT_WRITE; + if (HostWouldBlock(error) || HostTimedOut(error)) return MBEDTLS_ERR_SSL_WANT_WRITE; if (HostReset(error)) return MBEDTLS_ERR_NET_CONN_RESET; session.hostError = error; return MBEDTLS_ERR_NET_SEND_FAILED; @@ -367,7 +373,6 @@ struct Session { static int Receive(void* argument, unsigned char* buffer, std::size_t length) { auto& session = *static_cast(argument); const auto chunk = static_cast(std::min(length, INT_MAX)); - session.timedOut = false; for (;;) { #ifdef _WIN32 const auto received = ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); @@ -377,41 +382,49 @@ struct Session { if (received >= 0) return static_cast(received); const int error = HostError(); if (HostInterrupted(error)) continue; - session.timedOut = HostTimedOut(session.socket, error); - if (session.timedOut || HostWouldBlock(error)) return MBEDTLS_ERR_SSL_WANT_READ; + if (HostWouldBlock(error) || HostTimedOut(error)) return MBEDTLS_ERR_SSL_WANT_READ; if (HostReset(error)) return MBEDTLS_ERR_NET_CONN_RESET; session.hostError = error; return MBEDTLS_ERR_NET_RECV_FAILED; } } - int VerifyCode() const { + void Verify() { const auto flags = mbedtls_ssl_get_verify_result(&ssl); - if (!(verifyOptions & FlagServerVerify)) return 0; - if ((verifyOptions & FlagKnownCaCheck) && (flags & MBEDTLS_X509_BADCERT_NOT_TRUSTED)) return ErrorUnknownCa; - if ((verifyOptions & FlagCnCheck) && (flags & MBEDTLS_X509_BADCERT_CN_MISMATCH)) return ErrorCnCheck; - if ((verifyOptions & FlagNotAfterCheck) && (flags & MBEDTLS_X509_BADCERT_EXPIRED)) return ErrorNotAfterCheck; - if ((verifyOptions & FlagNotBeforeCheck) && (flags & MBEDTLS_X509_BADCERT_FUTURE)) return ErrorNotBeforeCheck; + failedOptions = 0; + verifyResult = 0; + if (!(verifyOptions & FlagServerVerify)) return; + if ((verifyOptions & FlagKnownCaCheck) && (flags & MBEDTLS_X509_BADCERT_NOT_TRUSTED)) failedOptions |= FlagKnownCaCheck; + if ((verifyOptions & FlagCnCheck) && (flags & MBEDTLS_X509_BADCERT_CN_MISMATCH)) failedOptions |= FlagCnCheck; + if ((verifyOptions & FlagNotAfterCheck) && (flags & MBEDTLS_X509_BADCERT_EXPIRED)) failedOptions |= FlagNotAfterCheck; + if ((verifyOptions & FlagNotBeforeCheck) && (flags & MBEDTLS_X509_BADCERT_FUTURE)) failedOptions |= FlagNotBeforeCheck; constexpr std::uint32_t optional = MBEDTLS_X509_BADCERT_NOT_TRUSTED | MBEDTLS_X509_BADCERT_CN_MISMATCH | MBEDTLS_X509_BADCERT_EXPIRED | MBEDTLS_X509_BADCERT_FUTURE; - return (flags & ~optional) ? ErrorInvalidCert : 0; + if (failedOptions & FlagKnownCaCheck) verifyResult = ErrorUnknownCa; + else if (failedOptions & FlagCnCheck) verifyResult = ErrorCnCheck; + else if (failedOptions & FlagNotAfterCheck) verifyResult = ErrorNotAfterCheck; + else if (failedOptions & FlagNotBeforeCheck) verifyResult = ErrorNotBeforeCheck; + else if (flags & ~optional) verifyResult = ErrorInvalidCert; } - int Code(int result) const { + int Code(int result, Operation operation) { switch (result) { - case MBEDTLS_ERR_SSL_WANT_READ: return timedOut ? ErrorReadTimeout : ErrorWantPollIn; - case MBEDTLS_ERR_SSL_WANT_WRITE: return timedOut ? 0 : ErrorWantPollOut; - case MBEDTLS_ERR_SSL_CONN_EOF: - case MBEDTLS_ERR_NET_CONN_RESET: return ErrorSocketClosed; + case MBEDTLS_ERR_SSL_WANT_READ: return ErrorWantPollIn; + case MBEDTLS_ERR_SSL_WANT_WRITE: return ErrorWantPollOut; + case MBEDTLS_ERR_NET_CONN_RESET: + reset = true; + if (operation == Operation::Write) return ErrorNetPipe; + if (operation == Operation::Connect) return ErrorSocketClosed; + return ErrorNetReset; + case MBEDTLS_ERR_SSL_CONN_EOF: return operation == Operation::Connect ? ErrorSocketClosed : 0; case MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE: return ErrorFatalAlert; + case MBEDTLS_ERR_SSL_INVALID_MAC: return operation == Operation::Read ? ErrorBadRecord : 0; default: return 0; } } [[noreturn]] void Fail(const char* function, int error) const { if (currentPoolExhausted || error == MBEDTLS_ERR_SSL_ALLOC_FAILED) throw std::bad_alloc(); - if (error == MBEDTLS_ERR_SSL_WANT_WRITE && timedOut) - throw std::runtime_error(std::string(function) + ": the socket's send timeout expired; the console's error code is unknown"); char text[160]; mbedtls_strerror(error, text, sizeof(text)); auto message = std::string(function) + ": " + text; @@ -426,7 +439,7 @@ struct Session { using SslVerifyCallback = int (APS5_VABI *)(int, unsigned int, void**, int, void*); -constexpr int SslVersionNone = 0; +constexpr int SslVersionSsl30 = 2; constexpr int SslVersionTls12 = 5; struct SslConnection { @@ -436,7 +449,7 @@ struct SslConnection { SslVerifyCallback verifyCallback; void* verifyArgument; std::uint32_t verifyOptions = InitialVerifyOptions; - int minimumVersion = SslVersionNone; + int minimumVersion = SslVersionTls12; std::string alpn; std::shared_ptr session; }; @@ -463,26 +476,22 @@ Context& GetContext(int handle) { return *found->second; } -SslConnection& RequireConnection(const char* function, int sslConnectionId) { +SslConnection* FindConnection(int sslConnectionId) { const auto connection = State().connections.find(sslConnectionId); - if (connection == State().connections.end()) throw std::invalid_argument(std::string(function) + ": unknown connection"); - if (!State().contexts.contains(connection->second.context)) { - throw std::invalid_argument(std::string(function) + ": the connection's context was terminated"); - } - return connection->second; + if (connection == State().connections.end()) return nullptr; + if (!State().contexts.contains(connection->second.context)) return nullptr; + return &connection->second; } -std::shared_ptr RequireSession(const char* function, int sslConnectionId) { - std::lock_guard lock(State().mutex); - auto& connection = RequireConnection(function, sslConnectionId); - if (!connection.session) throw std::logic_error(std::string(function) + ": the connection is not connected"); - return connection.session; +SslConnection& RequireConnection(const char* function, int sslConnectionId) { + auto* connection = FindConnection(sslConnectionId); + if (!connection) throw std::invalid_argument(std::string(function) + ": unknown connection"); + return *connection; } -void RequireVerified(const char* function, const Session& session) { - if (!session.verified) throw std::logic_error(std::string(function) + ": the connection is not connected"); - if (session.verifyResult != 0) throw std::logic_error(std::string(function) + ": the connection failed verification"); - if (session.closed) throw std::logic_error(std::string(function) + ": the connection was closed"); +void RequireUsable(const char* function, const Session& session) { + if (session.verifyResult != 0 && session.verifyResult != ErrorCallbackCheck) + throw std::logic_error(std::string(function) + ": the connection failed verification; the console's behaviour is unknown"); } void RegisterPeer(Session& session) { @@ -554,6 +563,17 @@ const mbedtls_x509_crt* RequireCertificate(const char* function, int sslCtxId, v throw std::invalid_argument(std::string(function) + ": unknown certificate"); } +const char* LongAttributeName(const char* shortName) { + static constexpr std::pair names[] = { + {"CN", "commonName"}, {"C", "countryName"}, {"L", "localityName"}, {"ST", "stateOrProvinceName"}, + {"O", "organizationName"}, {"OU", "organizationalUnitName"}, {"SN", "surname"}, {"GN", "givenName"}, + {"DC", "domainComponent"}, {"uid", "userId"}, + }; + for (const auto& [name, longName] : names) + if (std::strcmp(shortName, name) == 0) return longName; + return shortName; +} + void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool subject) { std::lock_guard lock(State().mutex); auto& context = GetContext(sslCtxId); @@ -565,7 +585,7 @@ void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool su std::string oid; const char* shortName = nullptr; if (mbedtls_oid_get_attr_short_name(&node->oid, &shortName) == 0 && shortName) { - oid = shortName; + oid = LongAttributeName(shortName); } else { char numeric[64] = {}; if (mbedtls_oid_get_numeric_string(numeric, sizeof(numeric), &node->oid) > 0) { @@ -588,6 +608,7 @@ void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool su extern "C" { int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { + if (poolSize == 0) return ErrorKernelInvalid; auto& state = State(); std::lock_guard lock(state.mutex); if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL context handles exhausted"); @@ -600,18 +621,18 @@ int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { int APS5_VABI sceSslTerm_nid_postfix(int sslCtxId) { auto& state = State(); std::lock_guard lock(state.mutex); - state.contexts.erase(sslCtxId); + if (!state.contexts.erase(sslCtxId)) return ErrorInvalidId; return 0; } int APS5_VABI sceSslGetMemoryPoolStats(int sslCtxId, SceSsl::MemoryPoolStats* stats) { - if (!stats) return static_cast(0x8095F007); + if (!stats) return ErrorInvalidValue; auto& state = State(); std::lock_guard lock(state.mutex); const auto found = state.contexts.find(sslCtxId); if (found == state.contexts.end()) { std::memset(stats, 0, sizeof(*stats)); - return static_cast(0x8095F006); + return ErrorInvalidId; } auto& pool = found->second->pool; std::lock_guard poolLock(pool.mutex); @@ -757,7 +778,7 @@ int APS5_VABI sceSslFreeCaCerts(int sslCtxId, SceSsl::CaCerts* certificates) { } int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) { - if (!host) throw std::invalid_argument("sceSslCreateConnection: null host"); + if (!host) return ErrorInvalidValue; std::lock_guard lock(State().mutex); if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; auto& state = State(); @@ -771,58 +792,74 @@ int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) int APS5_VABI sceSslDeleteConnection(int sslConnectionId) { std::shared_ptr session; std::lock_guard lock(State().mutex); - const auto found = State().connections.find(sslConnectionId); - if (found == State().connections.end()) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); - session = std::move(found->second.session); - State().connections.erase(found); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + session = std::move(connection->session); + State().connections.erase(sslConnectionId); return 0; } int APS5_VABI sceSslSetVerifyCallback(int sslConnectionId, SslVerifyCallback cbfunc, void* userArg) { std::lock_guard lock(State().mutex); - SslConnection& connection = RequireConnection(__func__, sslConnectionId); - connection.verifyCallback = cbfunc; - connection.verifyArgument = userArg; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + connection->verifyCallback = cbfunc; + connection->verifyArgument = userArg; return 0; } -int APS5_VABI sceSslConnect(int sslConnectionId) { - std::shared_ptr session; +} + +namespace { + +int RunCallback(Session& session, std::unique_lock& sessionLock, SslVerifyCallback callback, void* argument) { + session.verifying = true; + sessionLock.unlock(); + const int decision = callback(session.contextHandle, session.failedOptions, session.peerHandles.data(), + static_cast(session.peerHandles.size()), argument); + sessionLock.lock(); + session.verifying = false; + return decision < 0 ? ErrorCallbackCheck : 0; +} + +int Establish(const char* function, int sslConnectionId, std::shared_ptr& session) { SslVerifyCallback callback = nullptr; void* callbackArgument = nullptr; { auto& state = State(); std::lock_guard lock(state.mutex); - auto& connection = RequireConnection(__func__, sslConnectionId); - if (!connection.session) { - auto context = state.contexts.at(connection.context); + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (!connection->session) { + auto context = state.contexts.at(connection->context); std::shared_ptr owner; std::uintptr_t native = 0; - if (!KernelLookupSocketNative_nid_no_patch(connection.socket, &owner, &native)) - throw std::invalid_argument("sceSslConnect: descriptor " + std::to_string(connection.socket) + " is not a socket"); + if (!KernelLookupSocketNative_nid_no_patch(connection->socket, &owner, &native)) return ErrorNetBadDescriptor; PoolScope scope(context->pool); - session = std::make_shared(std::move(context), std::move(owner), native); - session->Configure(connection.host, connection.alpn, connection.verifyOptions); - connection.session = session; + session = std::make_shared(connection->context, std::move(context), std::move(owner), native); + session->Configure(connection->host, connection->alpn, connection->verifyOptions); + connection->session = session; } else { - session = connection.session; + session = connection->session; } - callback = connection.verifyCallback; - callbackArgument = connection.verifyArgument; + callback = connection->verifyCallback; + callbackArgument = connection->verifyArgument; } std::unique_lock sessionLock(session->mutex); - if (session->verified && session->verifyResult != 0) return session->verifyResult; - if (session->closed) throw std::logic_error("sceSslConnect: the connection was closed"); - if (session->verified) return 0; - if (session->verifying) throw std::logic_error("sceSslConnect: the verify callback is running"); - int code = 0; + if (session->verifying) throw std::logic_error(std::string(function) + ": the verify callback is running"); + if (session->verified) { + if (session->verifyResult == ErrorCallbackCheck && callback) session->verifyResult = RunCallback(*session, sessionLock, callback, callbackArgument); + return session->verifyResult; + } + if (session->gone) return 0; { PoolScope scope(session->context->pool); if (!session->handshaken) { const int result = mbedtls_ssl_handshake(&session->ssl); - if (result == MBEDTLS_ERR_SSL_WANT_READ || result == MBEDTLS_ERR_SSL_WANT_WRITE) { - if (const int pending = session->Code(result)) return pending; - session->Fail(__func__, result); + if (result == MBEDTLS_ERR_SSL_WANT_READ || result == MBEDTLS_ERR_SSL_WANT_WRITE) return session->Code(result, Operation::Connect); + if (result == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE) { + session->gone = true; + return ErrorFatalAlert; } if (result != 0) { { @@ -830,55 +867,85 @@ int APS5_VABI sceSslConnect(int sslConnectionId) { const auto found = State().connections.find(sslConnectionId); if (found != State().connections.end() && found->second.session == session) found->second.session.reset(); } - if (const int failure = session->Code(result)) return failure; - session->Fail(__func__, result); + if (const int failure = session->Code(result, Operation::Connect)) return failure; + session->Fail(function, result); } session->handshaken = true; const char* selected = mbedtls_ssl_get_alpn_protocol(&session->ssl); session->alpnSelected = selected ? selected : ""; } - code = session->VerifyCode(); + session->Verify(); + if (callback && session->verifyResult == ErrorInvalidCert) + throw std::runtime_error(std::string(function) + ": the chain failed a check no verify option names; what the console passes to the verify callback is unknown"); if (callback) RegisterPeer(*session); } - if (callback) { - session->verifying = true; - sessionLock.unlock(); - const int decision = callback(sslConnectionId, static_cast(code), session->peerHandles.data(), - static_cast(session->peerHandles.size()), callbackArgument); - sessionLock.lock(); - session->verifying = false; - code = decision < 0 ? ErrorCallbackCheck : 0; - } + if (callback) session->verifyResult = RunCallback(*session, sessionLock, callback, callbackArgument); session->verified = true; - session->verifyResult = code; - if (code != 0 && !session->closed) { + if (session->verifyResult != 0 && session->verifyResult != ErrorCallbackCheck) { PoolScope scope(session->context->pool); mbedtls_ssl_close_notify(&session->ssl); session->closed = true; } - return code; + return session->verifyResult; +} + +} + +extern "C" { + +int APS5_VABI sceSslConnect(int sslConnectionId) { + std::shared_ptr session; + return Establish(__func__, sslConnectionId, session); } int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) { - if (flags != 0 && flags != ReadPartial) throw std::invalid_argument("sceSslRead: unsupported flags " + std::to_string(flags)); if (!buf && len) throw std::invalid_argument("sceSslRead: null buffer"); - const auto session = RequireSession(__func__, sslConnectionId); + std::shared_ptr session; + if (const int code = Establish(__func__, sslConnectionId, session)) { + if (session) { + std::lock_guard sessionLock(session->mutex); + RequireUsable(__func__, *session); + } + return code; + } std::lock_guard sessionLock(session->mutex); - RequireVerified(__func__, *session); + if (session->gone || session->closed) return 0; + if (session->reset) return ErrorNetReset; PoolScope scope(session->context->pool); - const int result = mbedtls_ssl_read(&session->ssl, static_cast(buf), std::min(len, INT_MAX)); - if (result >= 0) return result; - if (result == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) return 0; - if (const int code = session->Code(result)) return code; - session->Fail(__func__, result); + auto* bytes = static_cast(buf); + const auto total = std::min(len, INT_MAX); + std::size_t received = 0; + while (received < total) { + const int result = mbedtls_ssl_read(&session->ssl, bytes + received, total - received); + if (result > 0) { + received += static_cast(result); + if (flags != 0) break; + continue; + } + if (result == 0 || result == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || result == MBEDTLS_ERR_SSL_CONN_EOF) break; + const int code = session->Code(result, Operation::Read); + if (code == 0) session->Fail(__func__, result); + if (received) break; + return code; + } + return static_cast(received); } int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) { - if (flags != 0) throw std::invalid_argument("sceSslWrite: unsupported flags " + std::to_string(flags)); + if (flags != 0) return ErrorInvalidValue; if (!buf && len) throw std::invalid_argument("sceSslWrite: null buffer"); - const auto session = RequireSession(__func__, sslConnectionId); + std::shared_ptr session; + if (const int code = Establish(__func__, sslConnectionId, session)) { + if (session) { + std::lock_guard sessionLock(session->mutex); + RequireUsable(__func__, *session); + } + return code; + } std::lock_guard sessionLock(session->mutex); - RequireVerified(__func__, *session); + if (session->gone) return ErrorSocketClosed; + if (session->closed) throw std::logic_error("sceSslWrite: the connection was closed; the console's behaviour is unknown"); + if (session->reset) return ErrorNetPipe; PoolScope scope(session->context->pool); const auto* bytes = static_cast(buf); const auto total = std::min(len, INT_MAX); @@ -886,9 +953,10 @@ int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) while (written < total) { const int result = mbedtls_ssl_write(&session->ssl, bytes + written, total - written); if (result < 0) { + const int code = session->Code(result, Operation::Write); + if (code == 0) session->Fail(__func__, result); if (written) break; - if (const int code = session->Code(result)) return code; - session->Fail(__func__, result); + return code; } written += static_cast(result); } @@ -899,15 +967,20 @@ int APS5_VABI sceSslClose(int sslConnectionId) { std::shared_ptr session; { std::lock_guard lock(State().mutex); - session = RequireConnection(__func__, sslConnectionId).session; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + session = connection->session; } if (!session) return 0; std::lock_guard sessionLock(session->mutex); - if (session->closed) return 0; - session->closed = true; - if (!session->handshaken) return 0; + if (session->gone || session->closed || !session->handshaken) return 0; + if (session->reset) return ErrorNetReset; PoolScope scope(session->context->pool); - mbedtls_ssl_close_notify(&session->ssl); + const int result = mbedtls_ssl_close_notify(&session->ssl); + if (result == MBEDTLS_ERR_SSL_WANT_WRITE || result == MBEDTLS_ERR_SSL_WANT_READ) return session->Code(result, Operation::Close); + if (result == MBEDTLS_ERR_NET_CONN_RESET) return session->Code(result, Operation::Close); + if (result != 0) session->Fail(__func__, result); + session->closed = true; return 0; } @@ -925,7 +998,6 @@ int APS5_VABI sceSslGetSerialNumber(int sslCtxId, void* sslCert, unsigned char* return ErrorInvalidArg; } std::memcpy(sboData, crt->serial.p, reqLen); - *sboLen = reqLen; return 0; } @@ -942,13 +1014,19 @@ int APS5_VABI sceSslFreeSslCertName(int sslCtxId, void* certName) { int APS5_VABI sceSslEnableVerifyOption(int sslConnectionId, std::uint32_t option) { std::lock_guard lock(State().mutex); - RequireConnection(__func__, sslConnectionId).verifyOptions |= option; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (option & ~VerifyOptionMask) return ErrorInvalidValue; + connection->verifyOptions |= option; return 0; } int APS5_VABI sceSslDisableVerifyOption(int sslConnectionId, std::uint32_t option) { std::lock_guard lock(State().mutex); - RequireConnection(__func__, sslConnectionId).verifyOptions &= ~option; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (option & ~VerifyOptionMask) return ErrorInvalidValue; + connection->verifyOptions &= ~option; return 0; } @@ -967,16 +1045,19 @@ const char* APS5_VABI sceSslGetAlpnSelected(int sslConnectionId) { int APS5_VABI sceSslSetAlpn(int sslConnectionId, const char* proto) { if (!proto) throw std::invalid_argument("sceSslSetAlpn: null protocol list"); std::lock_guard lock(State().mutex); - RequireConnection(__func__, sslConnectionId).alpn = proto; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (!*proto) return ErrorBadLength; + connection->alpn = proto; return 0; } int APS5_VABI sceSslSetMinSslVersion(int sslConnectionId, int version) { - if (version < SslVersionNone || version > SslVersionTls12) { - throw std::invalid_argument("sceSslSetMinSslVersion: unknown version " + std::to_string(version)); - } std::lock_guard lock(State().mutex); - RequireConnection(__func__, sslConnectionId).minimumVersion = version; + auto* connection = FindConnection(sslConnectionId); + if (!connection) return ErrorInvalidId; + if (version < SslVersionSsl30 || version > SslVersionTls12) return ErrorInvalidValue; + connection->minimumVersion = version; return 0; } diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index efda5060a0..4becc68d8c 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -18,6 +18,7 @@ int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, int APS5_VABI sceSslConnect(int); int APS5_VABI sceSslRead(int, void*, std::size_t, int); int APS5_VABI sceSslWrite(int, void*, std::size_t, int); +int APS5_VABI sceSslClose(int); int APS5_VABI sceSslEnableVerifyOption(int, std::uint32_t); int APS5_VABI sceSslDisableVerifyOption(int, std::uint32_t); int APS5_VABI sceSslSetMinSslVersion(int, int); @@ -101,7 +102,11 @@ int main() { Require(list.certs == nullptr && list.num == 0); constexpr int invalidId = static_cast(0x8095F006); - Require(Throws([&] { sceSslCreateConnection(context, 0, nullptr); })); + constexpr int invalidValue = static_cast(0x8095F007); + constexpr int badLength = static_cast(0x80951773); + constexpr int badDescriptor = static_cast(0x80410109); + Require(sceSslInit_nid_postfix(0) == static_cast(0x80020016)); + Require(sceSslCreateConnection(context, 0, nullptr) == invalidValue); Require(sceSslCreateConnection(context + 1000, 0, "example.com") == invalidId); const int connection = sceSslCreateConnection(context, 0, "example.com"); @@ -113,39 +118,51 @@ int main() { Require(sceSslEnableVerifyOption(connection, serverVerify | cnCheck) == 0); Require(sceSslDisableVerifyOption(connection, cnCheck) == 0); Require(sceSslDisableVerifyOption(connection, serverVerify | cnCheck) == 0); - Require(sceSslSetMinSslVersion(connection, 0) == 0); + Require(sceSslEnableVerifyOption(connection, 0x40) == invalidValue); + Require(sceSslDisableVerifyOption(connection, 0x1000) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 0) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 1) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 2) == 0); Require(sceSslSetMinSslVersion(connection, 5) == 0); - Require(Throws([&] { sceSslSetMinSslVersion(connection, -1); })); - Require(Throws([&] { sceSslSetMinSslVersion(connection, 6); })); + Require(sceSslSetMinSslVersion(connection, -1) == invalidValue); + Require(sceSslSetMinSslVersion(connection, 6) == invalidValue); Require(sceSslGetAlpnSelected(connection) == nullptr); Require(sceSslSetAlpn(connection, "http/1.1") == 0); - Require(sceSslSetAlpn(connection, "") == 0); + Require(sceSslSetAlpn(connection, "") == badLength); Require(Throws([&] { sceSslSetAlpn(connection, nullptr); })); Require(sceSslGetAlpnSelected(connection) == nullptr); char buffer[4]{}; - Require(Throws([&] { sceSslConnect(connection); })); - Require(Throws([&] { sceSslRead(connection, buffer, sizeof(buffer), 0); })); - Require(Throws([&] { sceSslWrite(connection, buffer, sizeof(buffer), 0); })); + Require(sceSslConnect(connection) == badDescriptor); + Require(sceSslRead(connection, buffer, sizeof(buffer), 0) == badDescriptor); + Require(sceSslWrite(connection, buffer, sizeof(buffer), 0) == badDescriptor); + Require(sceSslWrite(connection, buffer, sizeof(buffer), 1) == invalidValue); + Require(sceSslClose(connection) == 0); const int unknown = connection + 1000; - Require(Throws([&] { sceSslSetVerifyCallback(unknown, nullptr, nullptr); })); - Require(Throws([&] { sceSslEnableVerifyOption(unknown, serverVerify); })); - Require(Throws([&] { sceSslDisableVerifyOption(unknown, serverVerify); })); - Require(Throws([&] { sceSslSetMinSslVersion(unknown, 5); })); - Require(Throws([&] { sceSslSetAlpn(unknown, "http/1.1"); })); + Require(sceSslSetVerifyCallback(unknown, nullptr, nullptr) == invalidId); + Require(sceSslEnableVerifyOption(unknown, serverVerify) == invalidId); + Require(sceSslDisableVerifyOption(unknown, serverVerify) == invalidId); + Require(sceSslSetMinSslVersion(unknown, 5) == invalidId); + Require(sceSslSetAlpn(unknown, "http/1.1") == invalidId); Require(Throws([&] { sceSslGetAlpnSelected(unknown); })); - Require(Throws([&] { sceSslDeleteConnection(unknown); })); + Require(sceSslConnect(unknown) == invalidId); + Require(sceSslRead(unknown, buffer, sizeof(buffer), 0) == invalidId); + Require(sceSslWrite(unknown, buffer, sizeof(buffer), 0) == invalidId); + Require(sceSslClose(unknown) == invalidId); + Require(sceSslDeleteConnection(unknown) == invalidId); Require(sceSslTerm_nid_postfix(context) == 0); - Require(Throws([&] { sceSslSetVerifyCallback(connection, nullptr, nullptr); })); - Require(Throws([&] { sceSslEnableVerifyOption(connection, serverVerify); })); - Require(Throws([&] { sceSslDisableVerifyOption(connection, serverVerify); })); - Require(Throws([&] { sceSslSetMinSslVersion(connection, 5); })); - Require(Throws([&] { sceSslSetAlpn(connection, "http/1.1"); })); + Require(sceSslTerm_nid_postfix(context) == invalidId); + Require(sceSslSetVerifyCallback(connection, nullptr, nullptr) == invalidId); + Require(sceSslEnableVerifyOption(connection, serverVerify) == invalidId); + Require(sceSslDisableVerifyOption(connection, serverVerify) == invalidId); + Require(sceSslSetMinSslVersion(connection, 5) == invalidId); + Require(sceSslSetAlpn(connection, "http/1.1") == invalidId); Require(Throws([&] { sceSslGetAlpnSelected(connection); })); + Require(sceSslConnect(connection) == invalidId); + Require(sceSslClose(connection) == invalidId); Require(sceSslCreateConnection(context, 0, "example.com") == invalidId); - Require(sceSslDeleteConnection(connection) == 0); - Require(Throws([&] { sceSslDeleteConnection(connection); })); + Require(sceSslDeleteConnection(connection) == invalidId); Require(sceSslGetCaList(context, nullptr) == invalidArg); Require(sceSslFreeCaList(context, nullptr) == invalidArg); diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index 95e12521f7..28b894ddb4 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -39,6 +39,7 @@ int APS5_VABI sceSslGetPem(int, void*, char*, std::size_t*); namespace { constexpr int NotFound = static_cast(0x8095F004); +constexpr int InvalidId = static_cast(0x8095F006); void Check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); @@ -120,8 +121,8 @@ void ConnectionLifetime() { Check(sceSslSetVerifyCallback(retained, nullptr, nullptr) == 0, "credential unload invalidated the connection"); Check(sceSslFreeCaCerts(id, &bytes) == 0 && Stats(id).currentInuseSize == 0, "CA snapshot leaked with a retained connection"); Check(sceSslTerm_nid_postfix(id) == 0, "context termination failed with a retained connection"); - Throws([&] { sceSslSetVerifyCallback(retained, nullptr, nullptr); }); - Check(sceSslDeleteConnection(retained) == 0, "terminated context's connection cannot be released"); + Check(sceSslSetVerifyCallback(retained, nullptr, nullptr) == InvalidId, "terminated context's connection did not report an invalid id"); + Check(sceSslDeleteConnection(retained) == InvalidId, "terminated context's connection was still deletable"); Check(sceSslGetCaCerts(other, &empty) == NotFound, "unrelated context was invalidated"); Check(sceSslTerm_nid_postfix(other) == 0, "unrelated context termination failed"); } @@ -188,7 +189,7 @@ void CertificateReaders() { unsigned char serialData[8]{}; std::size_t serialLen = sizeof(serialData); Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == 0, "get serial failed"); - Check(serialLen == 3, "serial length mismatch"); + Check(serialLen == sizeof(serialData) && serialData[3] == 0, "a large enough serial buffer changed the caller's length"); Check(serialData[0] == 0x00 && serialData[1] == 0x80 && serialData[2] == 0xa1, "serial bytes mismatch"); Check(sceSslGetSerialNumber(id, cert, serialData, nullptr) == static_cast(0x8095177a), "null serial length accepted"); Throws([&] { sceSslGetSerialNumber(id, nullptr, serialData, &serialLen); }); @@ -217,11 +218,11 @@ void CertificateReaders() { char val[64]{}; std::size_t vlen = 0; Check(sceSslGetNameEntryInfo(id, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 0 info failed"); - Check(std::strcmp(oid, "C") == 0 && std::strcmp(val, "GB") == 0 && vlen == 2, "entry 0 mismatch"); + Check(std::strcmp(oid, "countryName") == 0 && std::strcmp(val, "GB") == 0 && vlen == 2, "entry 0 mismatch"); Check(sceSslGetNameEntryInfo(id, subject, 1, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 1 info failed"); - Check(std::strcmp(oid, "O") == 0 && std::strcmp(val, "AnyPS5 Tests") == 0 && vlen == 12, "entry 1 mismatch"); + Check(std::strcmp(oid, "organizationName") == 0 && std::strcmp(val, "AnyPS5 Tests") == 0 && vlen == 12, "entry 1 mismatch"); Check(sceSslGetNameEntryInfo(id, subject, 2, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 2 info failed"); - Check(std::strcmp(oid, "CN") == 0 && std::strcmp(val, "AnyPS5 fixture root") == 0 && vlen == 19, "entry 2 mismatch"); + Check(std::strcmp(oid, "commonName") == 0 && std::strcmp(val, "AnyPS5 fixture root") == 0 && vlen == 19, "entry 2 mismatch"); char smallVal[4]{}; std::size_t fullLen = 0; @@ -296,7 +297,7 @@ void CertificateReaders() { Throws([&] { sceSslGetPem(id, nullptr, pemBuf.data(), &writtenLen); }); Throws([&] { sceSslGetPem(id, fake, pemBuf.data(), &writtenLen); }); - Throws([&] { sceSslClose(1); }); + Check(sceSslClose(1) == InvalidId, "closing an unknown connection did not report an invalid id"); Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed"); Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); diff --git a/core/libs/tests/GuestSslPs5Readers.cpp b/core/libs/tests/GuestSslPs5Readers.cpp index 7f7bf6e909..caffe1cf3e 100644 --- a/core/libs/tests/GuestSslPs5Readers.cpp +++ b/core/libs/tests/GuestSslPs5Readers.cpp @@ -66,10 +66,10 @@ void Readers() { Throws([&] { sceSslGetSubjectName(other, certificate); }); Throws([&] { sceSslGetNameEntryCount(other, subject); }); Throws([&] { sceSslFreeSslCertName(other, subject); }); - std::array oid{}, value{}; + std::array 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"); + Check(std::strcmp(oid.data(), "organizationName") == 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"); diff --git a/core/libs/tests/GuestSslTransport.cpp b/core/libs/tests/GuestSslTransport.cpp index 9b5bb60bc5..93fcb1b901 100644 --- a/core/libs/tests/GuestSslTransport.cpp +++ b/core/libs/tests/GuestSslTransport.cpp @@ -63,14 +63,22 @@ int APS5_VABI sceNetSocketClose(int); namespace { constexpr int ParallelConnections = 4; +constexpr int ErrorInvalidId = static_cast(0x8095f006); +constexpr int ErrorInvalidValue = static_cast(0x8095f007); constexpr int ErrorCnCheck = static_cast(0x8095f00b); constexpr int ErrorUnknownCa = static_cast(0x8095f00c); +constexpr int ErrorNotAfterCheck = static_cast(0x8095f00d); +constexpr int ErrorNotBeforeCheck = static_cast(0x8095f00e); +constexpr int ErrorFatalAlert = static_cast(0x8095f010); constexpr int ErrorWantPollIn = static_cast(0x8095f013); constexpr int ErrorWantPollOut = static_cast(0x8095f014); constexpr int ErrorCallbackCheck = static_cast(0x8095f0fd); constexpr int ErrorSocketClosed = static_cast(0x8095170d); -constexpr int ErrorReadTimeout = static_cast(0x80951710); -constexpr std::uint32_t ServerVerify = 0x1, CnCheck = 0x4; +constexpr int ErrorNetBadDescriptor = static_cast(0x80410109); +constexpr int ErrorNetPipe = static_cast(0x80410120); +constexpr int ErrorNetReset = static_cast(0x80410136); +constexpr int ErrorBadLength = static_cast(0x80951773); +constexpr std::uint32_t ServerVerify = 0x1, CnCheck = 0x4, NotAfterCheck = 0x8, NotBeforeCheck = 0x10, KnownCaCheck = 0x20; constexpr int VersionTls12 = 5; constexpr int GuestSocketLevel = 0xffff; constexpr int GuestNonBlocking = 0x1200; @@ -78,7 +86,8 @@ constexpr int GuestReceiveTimeout = 0x1006; constexpr int WaitRounds = 2000; const char* ServerProtocols[] = {"http/1.1", nullptr}; -enum class Serve { Close, Exchange, Drop, Reset }; +enum class Serve { Close, Exchange, Drop, Reset, Alert }; +enum class Leaf { Valid, Expired, Future }; struct GuestTimeval { std::int64_t seconds; @@ -106,18 +115,20 @@ void Settle() { std::this_thread::sleep_for(std::chrono::milliseconds(5)); } struct CallbackRecord { int context; int result; - int connection = 0; + int id = 0; unsigned int error = 0; int count = 0; + int calls = 0; void* leaf = nullptr; std::string commonName; }; -int APS5_VABI RecordingCallback(int connection, unsigned int error, void** certificates, int count, void* argument) { +int APS5_VABI RecordingCallback(int id, unsigned int error, void** certificates, int count, void* argument) { auto& record = *static_cast(argument); - record.connection = connection; + record.id = id; record.error = error; record.count = count; + ++record.calls; if (count > 0 && certificates && certificates[0]) { record.leaf = certificates[0]; void* name = sceSslGetSubjectName(record.context, record.leaf); @@ -140,9 +151,13 @@ struct Server { mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context drbg; mbedtls_x509_crt certificate; + mbedtls_x509_crt expiredCertificate; + mbedtls_x509_crt futureCertificate; mbedtls_x509_crt authority; mbedtls_pk_context key; - mbedtls_ssl_config config; + mbedtls_pk_context expiredKey; + mbedtls_pk_context futureKey; + mbedtls_ssl_config configs[3]; std::uint16_t port = 0; std::string failure; @@ -151,9 +166,13 @@ struct Server { mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&drbg); mbedtls_x509_crt_init(&certificate); + mbedtls_x509_crt_init(&expiredCertificate); + mbedtls_x509_crt_init(&futureCertificate); mbedtls_x509_crt_init(&authority); mbedtls_pk_init(&key); - mbedtls_ssl_config_init(&config); + mbedtls_pk_init(&expiredKey); + mbedtls_pk_init(&futureKey); + for (auto& config : configs) mbedtls_ssl_config_init(&config); Require(mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, nullptr, 0) == 0, "seed server random generator"); Require(mbedtls_x509_crt_parse(&certificate, reinterpret_cast(ServerPem), sizeof(ServerPem)) == 0, "parse server certificate"); @@ -161,13 +180,26 @@ struct Server { "parse client authority"); Require(mbedtls_pk_parse_key(&key, reinterpret_cast(ServerKey), sizeof(ServerKey), nullptr, 0, mbedtls_ctr_drbg_random, &drbg) == 0, "parse server key"); - Require(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) == 0, - "configure server"); - mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &drbg); - Require(mbedtls_ssl_conf_own_cert(&config, &certificate, &key) == 0, "set server certificate"); - mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_REQUIRED); - mbedtls_ssl_conf_ca_chain(&config, &authority, nullptr); - Require(mbedtls_ssl_conf_alpn_protocols(&config, ServerProtocols) == 0, "set server protocols"); + Require(mbedtls_x509_crt_parse(&expiredCertificate, reinterpret_cast(ExpiredServerPem), sizeof(ExpiredServerPem)) == 0, + "parse expired server certificate"); + Require(mbedtls_pk_parse_key(&expiredKey, reinterpret_cast(ExpiredServerKey), sizeof(ExpiredServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &drbg) == 0, "parse expired server key"); + Require(mbedtls_x509_crt_parse(&futureCertificate, reinterpret_cast(FutureServerPem), sizeof(FutureServerPem)) == 0, + "parse future server certificate"); + Require(mbedtls_pk_parse_key(&futureKey, reinterpret_cast(FutureServerKey), sizeof(FutureServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &drbg) == 0, "parse future server key"); + mbedtls_x509_crt* leaves[] = {&certificate, &expiredCertificate, &futureCertificate}; + mbedtls_pk_context* keys[] = {&key, &expiredKey, &futureKey}; + for (int index = 0; index < 3; ++index) { + auto& config = configs[index]; + Require(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) == 0, + "configure server"); + mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &drbg); + Require(mbedtls_ssl_conf_own_cert(&config, leaves[index], keys[index]) == 0, "set server certificate"); + mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&config, &authority, nullptr); + Require(mbedtls_ssl_conf_alpn_protocols(&config, ServerProtocols) == 0, "set server protocols"); + } Require(mbedtls_net_bind(&listener, "127.0.0.1", "0", MBEDTLS_NET_PROTO_TCP) == 0, "bind loopback listener"); sockaddr_in address{}; #ifdef _WIN32 @@ -180,16 +212,21 @@ struct Server { } ~Server() { - mbedtls_ssl_config_free(&config); + for (auto& config : configs) mbedtls_ssl_config_free(&config); + mbedtls_pk_free(&futureKey); + mbedtls_pk_free(&expiredKey); mbedtls_pk_free(&key); mbedtls_x509_crt_free(&authority); + mbedtls_x509_crt_free(&futureCertificate); + mbedtls_x509_crt_free(&expiredCertificate); mbedtls_x509_crt_free(&certificate); mbedtls_ctr_drbg_free(&drbg); mbedtls_entropy_free(&entropy); mbedtls_net_free(&listener); } - void Serve(Serve mode, bool requireClientCertificate = true) { + void Serve(Serve mode, bool requireClientCertificate = true, Leaf leaf = Leaf::Valid) { + auto& config = configs[static_cast(leaf)]; mbedtls_ssl_conf_authmode(&config, requireClientCertificate ? MBEDTLS_SSL_VERIFY_REQUIRED : MBEDTLS_SSL_VERIFY_NONE); mbedtls_net_context client; mbedtls_net_init(&client); @@ -197,6 +234,8 @@ struct Server { mbedtls_ssl_init(&ssl); if (mbedtls_net_accept(&listener, &client, nullptr, 0, nullptr) != 0 || mbedtls_ssl_setup(&ssl, &config) != 0) { failure = "server accept or setup failed"; + } else if (mode == Serve::Alert) { + Alert(client); } else { mbedtls_ssl_set_bio(&ssl, &client, mbedtls_net_send, mbedtls_net_recv, nullptr); const int handshake = mbedtls_ssl_handshake(&ssl); @@ -209,6 +248,16 @@ struct Server { mbedtls_net_free(&client); } + void Alert(mbedtls_net_context& client) { + std::array hello{}; + if (mbedtls_net_recv(&client, hello.data(), hello.size()) <= 0) { + failure = "server did not receive the client hello before the alert"; + return; + } + const unsigned char alert[] = {0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x28}; + if (mbedtls_net_send(&client, alert, sizeof(alert)) != sizeof(alert)) failure = "server could not send the alert"; + } + void Reset(mbedtls_ssl_context& ssl, mbedtls_net_context& client) { unsigned char signal = 0; if (mbedtls_ssl_read(&ssl, &signal, 1) != 1) { @@ -291,14 +340,21 @@ int main(int argc, char** argv) { server.Serve(Serve::Close); server.Serve(Serve::Close); server.Serve(Serve::Close); + server.Serve(Serve::Close); server.Serve(Serve::Exchange); server.Serve(Serve::Exchange); server.Serve(Serve::Drop); server.Serve(Serve::Reset); + server.Serve(Serve::Alert); + server.Serve(Serve::Close, true, Leaf::Expired); + server.Serve(Serve::Close, true, Leaf::Expired); + server.Serve(Serve::Close, true, Leaf::Future); + server.Serve(Serve::Close, true, Leaf::Future); if (sockets.net) server.Serve(Serve::Exchange); for (int index = 0; index < ParallelConnections; ++index) server.Serve(Serve::Exchange); }); + Require(sceSslInit_nid_postfix(0) == static_cast(0x80020016), "an empty pool was accepted"); const int empty = sceSslInit_nid_postfix(256 * 1024); const int untrustedSocket = sockets.Open(server.port); const int untrusted = sceSslCreateConnection(empty, untrustedSocket, "localhost"); @@ -308,6 +364,12 @@ int main(int argc, char** argv) { Require(Throws([&] { sceSslRead(untrusted, scratch.data(), 1, 0); }), "a connection that failed verification was readable"); Require(sceSslClose(untrusted) == 0, "close a connection that failed verification"); Require(sceSslDeleteConnection(untrusted) == 0 && sockets.Close(untrustedSocket) == 0, "release the untrusted connection"); + Require(sceSslDeleteConnection(untrusted) == ErrorInvalidId && sceSslClose(untrusted) == ErrorInvalidId + && sceSslConnect(untrusted) == ErrorInvalidId && sceSslRead(untrusted, scratch.data(), 1, 0) == ErrorInvalidId + && sceSslWrite(untrusted, scratch.data(), 1, 0) == ErrorInvalidId && sceSslSetAlpn(untrusted, "h2") == ErrorInvalidId + && sceSslEnableVerifyOption(untrusted, CnCheck) == ErrorInvalidId && sceSslDisableVerifyOption(untrusted, CnCheck) == ErrorInvalidId + && sceSslSetMinSslVersion(untrusted, VersionTls12) == ErrorInvalidId && sceSslSetVerifyCallback(untrusted, nullptr, nullptr) == ErrorInvalidId, + "a deleted connection did not report an invalid id"); const int unverifiedSocket = sockets.Open(server.port); const int unverified = sceSslCreateConnection(empty, unverifiedSocket, "localhost"); @@ -322,13 +384,14 @@ int main(int argc, char** argv) { const int accepted = sceSslCreateConnection(empty, acceptedSocket, "localhost"); Require(sceSslSetVerifyCallback(accepted, RecordingCallback, &accepting) == 0, "set the accepting verify callback"); Require(sceSslConnect(accepted) == 0, "a verify callback that returns 0 did not accept the connection"); - Require(accepting.connection == accepted && accepting.error == static_cast(ErrorUnknownCa), - "the verify callback did not receive the connection and its verification result"); - Require(accepting.count == 1 && accepting.commonName == "CN=localhost", "the verify callback did not receive the peer chain"); + Require(accepting.id == empty && accepting.error == KnownCaCheck, "the verify callback did not receive the context id and the failed option"); + Require(accepting.count == 1 && accepting.commonName == "commonName=localhost", "the verify callback did not receive the peer chain"); + Require(sceSslConnect(accepted) == 0 && accepting.calls == 1, "a second connect after an accepted callback ran the callback again"); Require(sceSslRead(accepted, scratch.data(), scratch.size(), 0) == 0, "an accepted connection was not readable"); Require(sceSslDeleteConnection(accepted) == 0 && sockets.Close(acceptedSocket) == 0, "release the accepted connection"); Require(Throws([&] { sceSslGetSubjectName(empty, accepting.leaf); }), "a peer certificate handle outlived its connection"); Require(sceSslTerm_nid_postfix(empty) == 0, "release the context without CA"); + Require(sceSslTerm_nid_postfix(empty) == ErrorInvalidId, "a second context termination did not report an invalid id"); const int context = sceSslInit_nid_postfix(1024 * 1024); std::string root = RootPem; @@ -340,8 +403,9 @@ int main(int argc, char** argv) { Require(sceSslLoadCert(context, 1, &authorities, &certificate, &key) == 0, "load the fixture root and client certificate"); const auto loaded = InUse(context); + Require(sceSslCreateConnection(context, 1000, nullptr) == ErrorInvalidValue, "a null host was accepted"); const int closed = sceSslCreateConnection(context, 1000, "localhost"); - Require(Throws([&] { sceSslConnect(closed); }), "a handshake over a closed descriptor was attempted"); + Require(sceSslConnect(closed) == ErrorNetBadDescriptor, "a handshake over a closed descriptor did not report the bad descriptor"); Require(sceSslClose(closed) == 0, "close a connection that never connected"); Require(sceSslDeleteConnection(closed) == 0, "delete connection over a closed descriptor"); @@ -363,32 +427,45 @@ int main(int argc, char** argv) { const int rejected = sceSslCreateConnection(context, rejectedSocket, "localhost"); Require(sceSslSetVerifyCallback(rejected, RecordingCallback, &rejecting) == 0, "set the rejecting verify callback"); Require(sceSslConnect(rejected) == ErrorCallbackCheck, "a negative verify callback result did not fail the connect"); - Require(rejecting.error == 0 && rejecting.count == 1, "the verify callback did not see a passed chain"); - Require(sceSslConnect(rejected) == ErrorCallbackCheck, "a second connect did not repeat the callback result"); - Require(Throws([&] { sceSslRead(rejected, scratch.data(), 1, 0); }), "a rejected connection was readable"); + Require(rejecting.error == 0 && rejecting.count == 1 && rejecting.id == context, "the verify callback did not see a passed chain"); + Require(sceSslConnect(rejected) == ErrorCallbackCheck && rejecting.calls == 2, "a second connect did not run the callback again"); + Require(sceSslRead(rejected, scratch.data(), 1, 0) == ErrorCallbackCheck && rejecting.calls == 3, "a read after the rejection did not run the callback again"); + Require(sceSslWrite(rejected, scratch.data(), 1, 0) == ErrorCallbackCheck && rejecting.calls == 4, "a write after the rejection did not run the callback again"); + Require(sceSslClose(rejected) == 0, "close the rejected connection"); Require(sceSslDeleteConnection(rejected) == 0 && sockets.Close(rejectedSocket) == 0, "release the rejected connection"); + const int earlySocket = sockets.Open(server.port); + const int early = sceSslCreateConnection(context, earlySocket, "localhost"); + Require(sceSslClose(early) == 0, "close before the handshake"); + Require(sceSslRead(early, scratch.data(), scratch.size(), 0) == 0, "a read before the handshake did not run it and reach the close notify"); + Require(sceSslConnect(early) == 0, "a connect after the read-driven handshake did not return 0"); + Require(sceSslDeleteConnection(early) == 0 && sockets.Close(earlySocket) == 0, "release the early connection"); + const int descriptor = sockets.Open(server.port); const int connection = sceSslCreateConnection(context, descriptor, "localhost"); std::array buffer{}; + Require(sceSslSetMinSslVersion(connection, 0) == ErrorInvalidValue && sceSslSetMinSslVersion(connection, 6) == ErrorInvalidValue + && sceSslSetMinSslVersion(connection, 2) == 0, "the accepted minimum versions changed"); + Require(sceSslEnableVerifyOption(connection, 0x40) == ErrorInvalidValue && sceSslDisableVerifyOption(connection, 0x1000) == ErrorInvalidValue + && sceSslEnableVerifyOption(connection, 0x3f) == 0, "the verify option mask changed"); + Require(sceSslSetAlpn(connection, "") == ErrorBadLength, "an empty ALPN list was accepted"); Require(sceSslSetMinSslVersion(connection, VersionTls12) == 0 && sceSslSetAlpn(connection, "h2,http/1.1") == 0, "set version and ALPN"); Require(sceSslGetAlpnSelected(connection) == nullptr, "a protocol was selected before the handshake"); - Require(Throws([&] { sceSslRead(connection, buffer.data(), buffer.size(), 0); }), "read before the handshake"); - Require(Throws([&] { sceSslWrite(connection, buffer.data(), buffer.size(), 0); }), "write before the handshake"); Require(sceSslConnect(connection) == 0, "handshake with the loopback server"); Require(InUse(context) > loaded, "the connection is not charged to the context pool"); Require(sceSslConnect(connection) == 0, "a second connect after success did not return the same result"); const char* selected = sceSslGetAlpnSelected(connection); Require(selected && std::strcmp(selected, "http/1.1") == 0, "the negotiated ALPN protocol is not reported"); - Require(Throws([&] { sceSslWrite(connection, buffer.data(), 1, 1); }), "unknown write flags were accepted"); - Require(Throws([&] { sceSslRead(connection, buffer.data(), 1, 3); }), "peek was accepted"); + Require(sceSslWrite(connection, buffer.data(), 1, 1) == ErrorInvalidValue, "unknown write flags were accepted"); char request[] = "ping"; Require(sceSslWrite(connection, request, 4, 0) == 4, "write the request"); - Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the reply"); + Require(sceSslRead(connection, buffer.data(), 2, 1) == 2 && std::memcmp(buffer.data(), "po", 2) == 0, "a partial read did not return the available bytes"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 2 && std::memcmp(buffer.data(), "ng", 2) == 0, "a full read did not stop at the end of the stream"); Require(sceSslRead(connection, buffer.data(), buffer.size(), 1) == 0, "close notify did not end the stream"); Require(sceSslClose(connection) == 0, "close the connection"); - Require(Throws([&] { sceSslRead(connection, buffer.data(), buffer.size(), 0); }), "a closed connection was readable"); - Require(Throws([&] { sceSslConnect(connection); }), "a closed connection reconnected"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 0, "a read after the close did not return 0"); + Require(sceSslConnect(connection) == 0, "a connect after the close did not return 0"); + Require(selected && std::strcmp(sceSslGetAlpnSelected(connection), "http/1.1") == 0, "the ALPN protocol was forgotten after the close"); Require(sceSslClose(connection) == 0, "a second close failed"); Require(sceSslDeleteConnection(connection) == 0, "delete the connection"); Require(InUse(context) == loaded, "the connection's pool memory was not returned"); @@ -404,9 +481,10 @@ int main(int argc, char** argv) { Require(++rounds < WaitRounds, "a non-blocking handshake did not finish"); } Require(sceSslRead(polling, buffer.data(), buffer.size(), 0) == ErrorWantPollIn, "a non-blocking read with nothing to read did not ask to poll"); + Require(sceSslRead(polling, buffer.data(), buffer.size(), 1) == ErrorWantPollIn, "a non-blocking partial read with nothing to read did not ask to poll"); Require(sceSslWrite(polling, request, 4, 0) == 4, "write the non-blocking request"); int received; - for (rounds = 0; (received = sceSslRead(polling, buffer.data(), buffer.size(), 0)) == ErrorWantPollIn; Settle()) + for (rounds = 0; (received = sceSslRead(polling, buffer.data(), buffer.size(), 1)) == ErrorWantPollIn; Settle()) Require(++rounds < WaitRounds, "the non-blocking reply did not arrive"); Require(received == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the non-blocking reply"); for (rounds = 0; (received = sceSslRead(polling, buffer.data(), buffer.size(), 0)) == ErrorWantPollIn; Settle()) @@ -419,22 +497,58 @@ int main(int argc, char** argv) { const int dropped = sceSslCreateConnection(context, droppedSocket, "localhost"); Require(sceSslConnect(dropped) == 0, "handshake before the server drops the connection"); Require(sceSslRead(dropped, buffer.data(), buffer.size(), 0) == 0, "a stream that ended without close notify did not end the read"); + Require(sceSslRead(dropped, buffer.data(), buffer.size(), 1) == 0, "a second read after the drop did not return 0"); + Require(sceSslClose(dropped) == 0, "close after the drop"); Require(sceSslDeleteConnection(dropped) == 0 && sockets.Close(droppedSocket) == 0, "release the dropped connection"); const int resetSocket = sockets.Open(server.port); const int reset = sceSslCreateConnection(context, resetSocket, "localhost"); Require(sceSslConnect(reset) == 0, "handshake before the server resets the connection"); Require(sceSslWrite(reset, request, 1, 0) == 1, "write the byte the server resets after"); - Require(sceSslRead(reset, buffer.data(), buffer.size(), 0) == ErrorSocketClosed, "a reset did not report the closed socket"); + Require(sceSslRead(reset, buffer.data(), buffer.size(), 0) == ErrorNetReset, "a reset did not report the libSceNet reset code"); + Require(sceSslRead(reset, buffer.data(), buffer.size(), 1) == ErrorNetReset, "a second read after the reset changed the code"); + Require(sceSslWrite(reset, request, 4, 0) == ErrorNetPipe, "a write after the reset did not report the broken pipe"); + Require(sceSslConnect(reset) == 0, "a connect after the reset did not return 0"); + Require(sceSslClose(reset) == ErrorNetReset && sceSslClose(reset) == ErrorNetReset, "a close after the reset did not report the reset twice"); Require(sceSslDeleteConnection(reset) == 0 && sockets.Close(resetSocket) == 0, "release the reset connection"); + const int alertedSocket = sockets.Open(server.port); + const int alerted = sceSslCreateConnection(context, alertedSocket, "localhost"); + Require(sceSslConnect(alerted) == ErrorFatalAlert, "a fatal alert during the handshake did not report it"); + Require(sceSslConnect(alerted) == 0, "a connect after the fatal alert did not return 0"); + Require(sceSslRead(alerted, buffer.data(), buffer.size(), 0) == 0, "a read after the fatal alert did not return 0"); + Require(sceSslWrite(alerted, request, 4, 0) == ErrorSocketClosed, "a write after the fatal alert did not report the closed socket"); + Require(sceSslClose(alerted) == 0 && sceSslClose(alerted) == 0, "close after the fatal alert"); + Require(sceSslDeleteConnection(alerted) == 0 && sockets.Close(alertedSocket) == 0, "release the alerted connection"); + + const int expiredSocket = sockets.Open(server.port); + const int expired = sceSslCreateConnection(context, expiredSocket, "localhost"); + Require(sceSslConnect(expired) == 0, "an expired server certificate failed with the initial verify options"); + Require(sceSslRead(expired, buffer.data(), buffer.size(), 0) == 0, "close notify did not end the expired stream"); + Require(sceSslDeleteConnection(expired) == 0 && sockets.Close(expiredSocket) == 0, "release the expired connection"); + const int expiredCheckedSocket = sockets.Open(server.port); + const int expiredChecked = sceSslCreateConnection(context, expiredCheckedSocket, "localhost"); + Require(sceSslEnableVerifyOption(expiredChecked, NotAfterCheck) == 0, "enable the expiry check"); + Require(sceSslConnect(expiredChecked) == ErrorNotAfterCheck, "an expired server certificate passed the enabled expiry check"); + Require(sceSslDeleteConnection(expiredChecked) == 0 && sockets.Close(expiredCheckedSocket) == 0, "release the checked expired connection"); + const int futureSocket = sockets.Open(server.port); + const int future = sceSslCreateConnection(context, futureSocket, "localhost"); + Require(sceSslEnableVerifyOption(future, NotAfterCheck) == 0, "enable the expiry check on the future connection"); + Require(sceSslConnect(future) == 0, "a not yet valid server certificate failed without the start check"); + Require(sceSslDeleteConnection(future) == 0 && sockets.Close(futureSocket) == 0, "release the future connection"); + const int futureCheckedSocket = sockets.Open(server.port); + const int futureChecked = sceSslCreateConnection(context, futureCheckedSocket, "localhost"); + Require(sceSslEnableVerifyOption(futureChecked, NotBeforeCheck) == 0, "enable the start check"); + Require(sceSslConnect(futureChecked) == ErrorNotBeforeCheck, "a not yet valid server certificate passed the enabled start check"); + Require(sceSslDeleteConnection(futureChecked) == 0 && sockets.Close(futureCheckedSocket) == 0, "release the checked future connection"); + if (sockets.net) { const int timedSocket = sockets.Open(server.port); const GuestTimeval timeout{0, 200000}; Require(sockets.Set(timedSocket, GuestReceiveTimeout, &timeout, sizeof(timeout)) == 0, "set the receive timeout"); const int timed = sceSslCreateConnection(context, timedSocket, "localhost"); Require(sceSslConnect(timed) == 0, "handshake over the socket with a receive timeout"); - Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == ErrorReadTimeout, "an expired receive timeout did not report the timeout"); + Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == ErrorWantPollIn, "an expired receive timeout did not ask to poll"); Require(sceSslWrite(timed, request, 4, 0) == 4, "write the request after a timeout"); Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the reply after a timeout"); Require(sceSslRead(timed, buffer.data(), buffer.size(), 0) == 0, "close notify did not end the timed stream"); diff --git a/core/libs/tests/SslCertificateFixture.hpp b/core/libs/tests/SslCertificateFixture.hpp index bc5637db37..0c86103e10 100644 --- a/core/libs/tests/SslCertificateFixture.hpp +++ b/core/libs/tests/SslCertificateFixture.hpp @@ -91,3 +91,41 @@ g+KCUQ46CTPPM2bFyypaJZ3ZJq2hRANCAASOWLxgY2XcCm5WcZKzafvQHUuJX1Kt bMsNIbMUyke+Sp9+LrTGU52se7eyb3nq5AVuGiBdmoW5QkBMFx00hkyB -----END PRIVATE KEY----- )FIXTURE"; +inline constexpr char ExpiredServerPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIBxjCCAWygAwIBAgICEAMwCgYIKoZIzj0EAwIwQjELMAkGA1UEBhMCR0IxFTAT +BgNVBAoMDEFueVBTNSBUZXN0czEcMBoGA1UEAwwTQW55UFM1IGZpeHR1cmUgcm9v +dDAeFw0yNTA5MTQwMDAwMDBaFw0yNjA5MTkwMDAwMDBaMDgxCzAJBgNVBAYTAkdC +MRUwEwYDVQQKDAxBbnlQUzUgVGVzdHMxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABDvmM3W4JGu6KyrQLp/qYYvTX4MBUwWGIP9+ +VcRgPCO7jwtb0Aoaf6tSaG1Reax8J2rvTRRDRibTwZhg/6a7LKSjXDBaMBQGA1Ud +EQQNMAuCCWxvY2FsaG9zdDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUF +BwMBMB8GA1UdIwQYMBaAFFZolbTcGqYVMtT6BD37g9yqaAzcMAoGCCqGSM49BAMC +A0gAMEUCIFOAMPrcpBrmsJrbXaWzyCDe1UweszENyWAW4pZqs/hmAiEA/vehYXPI +exj4IIRU7Zt80wBXEjJNBk8KXmhO/UvPh/A= +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char ExpiredServerKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg4TeyA0YK3Bq0Rt2l +On7kv/wcghGMcWvA4f5xVsGJUcahRANCAAQ75jN1uCRruisq0C6f6mGL01+DAVMF +hiD/flXEYDwju48LW9AKGn+rUmhtUXmsfCdq700UQ0Ym08GYYP+muyyk +-----END PRIVATE KEY----- +)FIXTURE"; +inline constexpr char FutureServerPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIBxzCCAWygAwIBAgICEAQwCgYIKoZIzj0EAwIwQjELMAkGA1UEBhMCR0IxFTAT +BgNVBAoMDEFueVBTNSBUZXN0czEcMBoGA1UEAwwTQW55UFM1IGZpeHR1cmUgcm9v +dDAeFw0zNzExMjMwMDAwMDBaFw0zODEyMjcwMDAwMDBaMDgxCzAJBgNVBAYTAkdC +MRUwEwYDVQQKDAxBbnlQUzUgVGVzdHMxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABP1SqW8j3y1k1uDBPD2cFW3vDPOb09zGYm6g +mUpt2UXlx6WTsiohSzv9emkUSe/YJez3mLoEB4c/ohL0rNhdGTqjXDBaMBQGA1Ud +EQQNMAuCCWxvY2FsaG9zdDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUF +BwMBMB8GA1UdIwQYMBaAFFZolbTcGqYVMtT6BD37g9yqaAzcMAoGCCqGSM49BAMC +A0kAMEYCIQDG0pZG6c9W7tVIA9AoG8YVyQbBZ0krnjw4SlAfy1I3swIhAMIqV5JD +tjTwhuPqzDgkmObSwb6GGD1abVRLCDJJMjsZ +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char FutureServerKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgecaFHjyMpE80Z1ja +I7gmZUa8yQKqbgS9PZq3PQPD3h+hRANCAAT9UqlvI98tZNbgwTw9nBVt7wzzm9Pc +xmJuoJlKbdlF5celk7IqIUs7/XppFEnv2CXs95i6BAeHP6IS9KzYXRk6 +-----END PRIVATE KEY----- +)FIXTURE"; diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 8b1772a8a4..597a2246f7 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,9 +2,9 @@ ### Unknown function info -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. 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` and that `sceSslRead` returns 0 at the end of the stream. 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. `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: SharpProspero's `SceSslVersion` ends at TLS 1.2, and Mbed TLS 1.3 needs PSA crypto state that is process-wide rather than allocated from a context's pool. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, its validity dates and the host name the connection was created with, which is also sent as SNI, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits and codes); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C). A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. The result of the first completed `sceSslConnect` is final for the connection: a failure sends close_notify, later `sceSslConnect` calls return the same code, and `sceSslRead` and `sceSslWrite` throw. With a verify callback set it is called once after the handshake, outside the connection's lock, with the connection id, that code (0 when the chain passed), handles to a copy of the peer chain that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated), the chain length and the user argument; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and any other return accepts the connection whatever the code was, following SharpProspero's "a negative return fails the handshake" ([https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252)). Whether the id is the context or the connection, whether the second argument is that code or the bits of the failed checks, the chain element type, when the console runs the callback and whether a non-negative return overrides a failed check are unknown. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking. A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_READ_TIMEOUT` (0x80951710) from `sceSslConnect` and `sceSslRead`, and the call can be repeated; one whose `SO_SNDTIMEO` expires throws, as no write timeout code is known. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SSL_ERROR_SOCKET_CLOSED` (0x8095170D), as does a transport that ends without close_notify during the handshake, and a fatal alert returns `SSL_ERROR_FATAL_ALERT` (0x8095F010); when one of these ends the handshake the session is dropped and the next `sceSslConnect` starts over. Which situation returns which code is inferred: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/errno.h#L512-L546) gives the names and values but not when the console returns them, so `WANT_POLLIN` and `WANT_POLLOUT` rather than `SSL_ERROR_EAGAIN` (0x8095F00F) for a socket that would block, `SOCKET_CLOSED` rather than `SSL_ERROR_EOF` (0x8095177C) for a handshake cut short, `READ_TIMEOUT` for an expired receive timeout, `FATAL_ALERT` for a fatal alert, `INVALID_CERT` (0x8095F00A) for a verification failure no option names, and a repeated `sceSslConnect` returning the first result, 0 included, rather than an error are all unverified on a console. Other handshake and socket failures, a read or write before the handshake, after a failed verification or after `sceSslClose`, a descriptor that is not a socket, a null buffer, write flags other than 0 and read flags other than 0 and `MsgFlagPartialRead` (1) throw, as their console codes are unknown; `MsgFlagPeek` (3) is not implemented. `sceSslRead` returns what one Mbed TLS read delivers, up to `len`, with flags 0 and with `MsgFlagPartialRead` alike; SharpProspero describes `sceSslRead` as reading up to `len` but `MsgFlagPartialRead` as returning before the buffer is full, so whether flags 0 waits to fill the buffer is unknown. It returns 0 after the server's close_notify and also when the transport ends without one, as Mbed TLS reports both as the end of the stream. `sceSslWrite` sends all of `len`, capped at `INT_MAX`, and returns the count. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify. 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) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). It sends close_notify once if the handshake completed, marks the connection closed so later `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw, and leaves the socket to the title; on a connection that never connected, already failed verification or was already closed it returns 0. Whether the console closes the socket, what it returns for a connection that was never connected, and what happens when a non-blocking socket cannot take the close_notify at once (it is not retried) are unknown -- [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 +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. 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`. The behaviour below was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title); what differs for a title process is unknown. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006) from `sceSslCreateConnection`, and every connection call on an unknown or deleted connection id or on a connection whose context was terminated returns `SSL_ERROR_INVALID_ID`. `sceSslCreateConnection` returns `SSL_ERROR_INVALID_VALUE` (0x8095F007) for a null host and does not check the socket: `sceSslConnect` on a descriptor that is not a socket returns `SCE_NET_ERROR_EBADF` (0x80410109). `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: the console offers TLS 1.2 only (its ClientHello carries 12 suites and no supported_versions extension) and a TLS 1.3-only server gets `SSL_ERROR_FATAL_ALERT` (0x8095F010) from it, as from this build. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`, whose connections return `SSL_ERROR_INVALID_ID` from then on. The console's pool use for one connection with a two-certificate chain peaked at 80,544 bytes in a 256 KiB pool (5,568 after `sceSslInit`, 12,000 after loading one CA); a 16 KiB pool fails the connect with `SSL_ERROR_OUT_OF_MEMORY` (0x809517D5) there and here. The server's chain is checked against the CAs loaded with `sceSslLoadCert`, the host name the connection was created with (case-insensitively; an IP literal sends no SNI and matches an IP subject alternative name; an empty host sends an empty SNI and fails `CN_CHECK`) and, when enabled, its validity dates, and `sceSslConnect` returns the first failure the connection's verify options select (the verify option entry below lists the bits, codes and initial options); no root bundle is shipped and the console's built-in roots are unknown, so with no CA loaded a connection with the initial options returns `SSL_ERROR_UNKNOWN_CA` (0x8095F00C), as on the console. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. A failed verification is final: close_notify is sent, later `sceSslConnect` calls return the same code (also after the `WANT_POLLIN` rounds of a non-blocking socket), and `sceSslRead` and `sceSslWrite` throw, as the console's codes for them were not measured. With a verify callback set it is called once after the handshake, outside the connection's lock, with the context id, the bits of the enabled checks that failed (`KNOWN_CA_CHECK` 0x20, `CN_CHECK` 0x4, the date checks when enabled, 0 when the chain passed), handles to a copy of the peer chain as received (the leaf first, the root last when the server sent it) that the certificate readers of the connection's context accept until the connection is deleted (the handles stay registered in the context until it is terminated; the console frees them at the delete), the chain length and the user argument. A chain fault that no option names (`SSL_ERROR_INVALID_CERT` without a callback) throws when a callback is set, as the bits the console passes for it were not measured. A non-negative return accepts the connection whatever the bits were; a negative return fails the connect with `SSL_ERROR_APPLICATION_VERIFICATION_CALLBACK_CHECK` (0x8095F0FD) and the callback runs again, with the same chain, from every later `sceSslConnect`, `sceSslRead` and `sceSslWrite` of the connection, which return 0x8095F0FD while it keeps refusing and proceed once it accepts. A `sceSslConnect` from inside the callback, or from another thread while it runs, throws. `sceSslRead` and `sceSslWrite` before `sceSslConnect` run the handshake and verification themselves and return its code when it fails. A non-blocking socket that would block returns `SSL_ERROR_WANT_POLLIN` (0x8095F013) or `SSL_ERROR_WANT_POLLOUT` (0x8095F014) from `sceSslConnect`, `sceSslRead` and `sceSslWrite`; the next `sceSslConnect` resumes the handshake, and `sceSslWrite` returns the part it sent before blocking (the console returned 32 KiB of a 64 KiB write). A blocking libSceNet socket whose `SO_RCVTIMEO` expires returns `SSL_ERROR_WANT_POLLIN` as well, and the call can be repeated; `SSL_ERROR_READ_TIMEOUT` (0x80951710) is never returned. libkernel keeps its timeouts above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` with flags 0 reads until `len` bytes arrived or the stream ended and with any other flags (`MsgFlagPartialRead` 1 measured, 2 behaved the same) returns what the first record delivers; a non-blocking read that got some bytes returns them. It returns 0 after the server's close_notify and after a transport that ends without one, every later read 0 again, and `SSL_ERROR_EOF` (0x8095177C) and `SSL_ERROR_SOCKET_CLOSED` (0x8095170D) never from a read. A reset (`ECONNRESET`, `EPIPE`, `WSAECONNRESET`, `WSAECONNABORTED`, `WSAENETRESET`) returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) from `sceSslRead` and `SCE_NET_ERROR_EPIPE` (0x80410120) from `sceSslWrite`, on every later call too; a record that fails its MAC returns 0x80951D55 from `sceSslRead` after the fatal alert goes out. `sceSslWrite` accepts flags 0 only, `SSL_ERROR_INVALID_VALUE` otherwise, sends all of `len`, capped at `INT_MAX`, and returns the count; it keeps writing after the peer's close_notify or a stream that ended without one, as the console does. A fatal alert during the handshake returns `SSL_ERROR_FATAL_ALERT` and ends the transport: later `sceSslConnect` calls return 0, `sceSslRead` 0 and `sceSslWrite` `SSL_ERROR_SOCKET_CLOSED`, as measured. A transport that ends without close_notify or a reset during the handshake returns `SSL_ERROR_SOCKET_CLOSED`, the session is dropped and the next `sceSslConnect` starts over; those two handshake cases and `SSL_ERROR_INVALID_CERT` (0x8095F00A) for a verification failure no option names were not measured. Other handshake and socket failures, a read or write after a failed verification, a write after `sceSslClose`, a null buffer and a read or write from inside the callback throw, as their console codes are unknown. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify and returns `SSL_ERROR_INVALID_ID` when repeated. `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, which then returns `SSL_ERROR_INVALID_ID`; a second `sceSslTerm` returns `SSL_ERROR_INVALID_ID` and `sceSslInit` with a pool size of 0 returns `SCE_KERNEL_ERROR_EINVAL` (0x80020016). `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. The console's first `sceSslConnect` in a process took 23.8 s before its ClientHello left, a one-time initialisation not emulated. `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) - takes the connection id, as in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252). Measured on 13.40 as the connection entry describes: it sends close_notify once if the handshake completed and returns 0, 0 again when repeated and 0 on a connection that never connected, already failed verification or lost its transport to a fatal alert; a close before the handshake changes nothing, so the connection can still connect. After the close `sceSslConnect` returns 0 and `sceSslRead` 0, as on the console; `sceSslWrite` after it throws, as the console's code was not measured. A close_notify that a non-blocking socket cannot take at once returns `SSL_ERROR_WANT_POLLOUT` (0x8095F014) and the close can be repeated; after a reset it returns `SCE_NET_ERROR_ECONNRESET` (0x80410136) every time. The socket is left to the title; an unknown connection returns `SSL_ERROR_INVALID_ID` (0x8095F006). Whether the console closes the socket is unknown +- [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, writing the attribute long names the console writes (`commonName`, `organizationName`, `countryName`, measured on 13.40 through the verify callback's chain handles; numeric strings for attributes Mbed TLS cannot name) and value strings with `valueLen` set to the value's length. 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). `sceSslGetSerialNumber` with a buffer large enough leaves `*sboLen` as the caller set it, as measured on 13.40 with a 32-byte buffer. The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings, 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` +- [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 behaviour was measured on a PS5 on system software 13.40 with a ps5-payload-dev payload (every call printed with its return, a TLS 1.2 test server on the LAN presenting leaves issued by the test fixture root loaded with `sceSslLoadCert`, from an elfldr process rather than a title). 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 `SERVER_VERIFY`, `CN_CHECK` and `KNOWN_CA_CHECK` (0x25): the console accepted an expired and a not-yet-valid server certificate until 0x8 or 0x10 was enabled, which then returned `SSL_ERROR_NOT_AFTER_CHECK` (0x8095F00D) and `SSL_ERROR_NOT_BEFORE_CHECK` (0x8095F00E); whether 0x2 (`CLIENT_VERIFY`) starts enabled was not determined and it is kept off. A check that is disabled is not reported, whatever the chain. Bits outside 0x3F return `SSL_ERROR_INVALID_VALUE` (0x8095F007) from both calls. Disabling 0x1, 0x4 or 0x20 from the payload stopped its process through the kernel's debug exception path (`mDBG: Sending signal ... signo: 0xa0020101`, 6 of 6 attempts; disabling 0x2, 0x8 and 0x10 returned 0), so whether a title may clear them is unknown; this build clears them, as titles import the call. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values 2 (SSL 3.0) to 5 (TLS 1.2) and returns `SSL_ERROR_INVALID_VALUE` for 0, 1 and anything above 5; the console has no TLS 1.3 value. 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, which matches the console offering TLS 1.2 only. `sceSslSetAlpn` stores the string up to its terminator, split on commas into the protocols offered in the handshake (the console sent `h2,http/1.1` as the two names and a wire-format string as one name), a second call replaces the list, an empty string returns `SSL_ERROR_BAD_LENGTH` (0x80951773), and a null list throws (not measured). `sceSslGetAlpnSelected` returns the protocol the server selected, still after `sceSslClose`, 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 return `SSL_ERROR_INVALID_ID` (0x8095F006) from the four setters and throw from `sceSslGetAlpnSelected`, whose console return was not measured - [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