diff --git a/core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp b/core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp index 05492ffbd0..4f91746ed6 100644 --- a/core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp +++ b/core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp @@ -634,10 +634,6 @@ int APS5_VABI ASN1_OCTET_STRING_free_nid_postfix() { NotImplemented_nid_no_patch(__func__); } -int APS5_VABI tls_config_parse_protocols_nid_postfix() { - NotImplemented_nid_no_patch(__func__); -} - int APS5_VABI SSL_CTX_set_post_handshake_auth_nid_postfix() { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt index ee7d8d940a..45cb8215a0 100644 --- a/core/libs/prx/libSceLibreSsl3/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl3/CMakeLists.txt @@ -236,6 +236,20 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL "Linux" AND EXISTS "${LIBS_INCLU set_tests_properties(guest_libressl3_mixed_evp PROPERTIES TIMEOUT 20) endif() +target_sources(libSceLibreSsl3 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/TlsProtocols.cpp) +if(BUILD_TESTING) + add_test_executable(guest_libressl3_tls_protocols_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/TlsProtocols.cpp) + target_include_directories(guest_libressl3_tls_protocols_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl3_tls_protocols_tests PRIVATE libSceLibreSsl3 libc) + configure_windows_unwind(guest_libressl3_tls_protocols_tests) + add_test(NAME guest_libressl3_tls_protocols COMMAND guest_libressl3_tls_protocols_tests) + set_tests_properties(guest_libressl3_tls_protocols PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_libressl3_tls_protocols PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_sources(libSceLibreSsl3 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/RsaSetters.cpp) if(BUILD_TESTING) add_test_executable(guest_libressl3_rsa_setters_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/RsaSetters.cpp) diff --git a/core/libs/prx/libSceLibreSsl3/include/Api.hpp b/core/libs/prx/libSceLibreSsl3/include/Api.hpp index fc6595e81f..bcce1ccbf2 100644 --- a/core/libs/prx/libSceLibreSsl3/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl3/include/Api.hpp @@ -376,7 +376,7 @@ int APS5_VABI EVP_PKEY_cmp_nid_postfix(const LibreSsl3EvpPkey* first, const Libr int APS5_VABI d2i_ASN1_SEQUENCE_ANY_nid_postfix(); int APS5_VABI PKCS5_PBKDF2_HMAC_nid_postfix(); int APS5_VABI ASN1_OCTET_STRING_free_nid_postfix(); -int APS5_VABI tls_config_parse_protocols_nid_postfix(); +int APS5_VABI tls_config_parse_protocols_nid_postfix(std::uint32_t* protocols, const char* text); int APS5_VABI SSL_CTX_set_post_handshake_auth_nid_postfix(); int APS5_VABI tls_config_insecure_noverifyname_nid_postfix(); int APS5_VABI ASN1_TYPE_free_nid_postfix(); diff --git a/core/libs/prx/libSceLibreSsl3/src/TlsProtocols.cpp b/core/libs/prx/libSceLibreSsl3/src/TlsProtocols.cpp new file mode 100644 index 0000000000..cbeca33e65 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/src/TlsProtocols.cpp @@ -0,0 +1,70 @@ +#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp" +#include "prx/libc/libc.hpp" +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint32_t SupportedProtocols = 0x18; +constexpr std::array, 9> ProtocolNames{{ + {"all", SupportedProtocols}, {"legacy", SupportedProtocols}, + {"default", SupportedProtocols}, {"secure", SupportedProtocols}, + {"tlsv1", SupportedProtocols}, {"tlsv1.0", 0x02}, + {"tlsv1.1", 0x04}, {"tlsv1.2", 0x08}, {"tlsv1.3", 0x10} +}}; + +bool EqualName(std::string_view text, std::string_view name) { + if (text.size() != name.size()) return false; + for (std::size_t index = 0; index < text.size(); ++index) { + const unsigned char byte = static_cast(text[index]); + const unsigned char folded = byte >= 'A' && byte <= 'Z' ? byte + ('a' - 'A') : byte; + if (folded != static_cast(name[index])) return false; + } + return true; +} + +} + +extern "C" int APS5_VABI tls_config_parse_protocols_nid_postfix(std::uint32_t* protocols, const char* text) { + if (!protocols) throw std::invalid_argument("tls_config_parse_protocols: null output"); + if (!text) { + *protocols = SupportedProtocols; + return 0; + } + char* const copy = strdup_nid_postfix(text); + if (!copy) return -1; + std::string_view remaining(copy); + std::uint32_t result = 0; + for (;;) { + const std::size_t separator = remaining.find_first_of(",:"); + std::string_view token = remaining.substr(0, separator); + while (!token.empty() && (token.front() == ' ' || token.front() == '\t')) token.remove_prefix(1); + const bool exclude = !token.empty() && token.front() == '!'; + if (exclude) token.remove_prefix(1); + std::uint32_t selected = 0; + for (const auto& [name, mask] : ProtocolNames) { + if (EqualName(token, name)) { + selected = mask; + break; + } + } + if (!selected) { + free_nid_postfix(copy); + return -1; + } + if (exclude) { + if (!result) result = SupportedProtocols; + result &= ~selected; + } else { + result |= selected; + } + if (separator == std::string_view::npos) break; + remaining.remove_prefix(separator + 1); + } + *protocols = result; + free_nid_postfix(copy); + return 0; +} diff --git a/core/libs/prx/libSceLibreSsl3/tests/TlsProtocols.cpp b/core/libs/prx/libSceLibreSsl3/tests/TlsProtocols.cpp new file mode 100644 index 0000000000..2db353338f --- /dev/null +++ b/core/libs/prx/libSceLibreSsl3/tests/TlsProtocols.cpp @@ -0,0 +1,189 @@ +#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::size_t checks = 0; +std::size_t cases = 0; +std::size_t attempts = 0; +std::size_t releases = 0; +void* live = nullptr; +std::size_t liveSize = 0; +bool failAllocation = false; +const std::uint32_t* observedOutput = nullptr; +std::uint32_t expectedAtRelease = 0; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + ++checks; + if (condition) return; + std::fprintf(stderr, "TLS protocol check failed at line %u, case %zu\n", location.line(), cases); + std::abort(); +} + +void* APS5_VABI Allocate_nid_no_patch(std::size_t size) { + ++attempts; + Require(live == nullptr); + if (failAllocation) return nullptr; + auto* raw = static_cast(std::malloc(size + 32)); + Require(raw != nullptr); + std::memset(raw, 0xa5, size + 32); + live = raw + 16; + liveSize = size; + return live; +} + +void APS5_VABI Release_nid_no_patch(void* pointer) { + if (!pointer) return; + Require(pointer == live); + if (observedOutput) Require(*observedOutput == expectedAtRelease); + auto* raw = static_cast(pointer) - 16; + for (std::size_t i = 0; i < 16; ++i) Require(raw[i] == 0xa5 && raw[16 + liveSize + i] == 0xa5); + std::free(raw); + live = nullptr; + ++releases; +} + +void* APS5_VABI UnexpectedCalloc_nid_no_patch(std::size_t, std::size_t) { throw std::runtime_error("unexpected calloc"); } +void* APS5_VABI UnexpectedRealloc_nid_no_patch(void*, std::size_t) { throw std::runtime_error("unexpected realloc"); } +void* APS5_VABI UnexpectedAlign_nid_no_patch(std::size_t, std::size_t) { throw std::runtime_error("unexpected align"); } +void* APS5_VABI UnexpectedRealign_nid_no_patch(void*, std::size_t, std::size_t) { throw std::runtime_error("unexpected realign"); } +int APS5_VABI UnexpectedPosixAlign_nid_no_patch(void**, std::size_t, std::size_t) { throw std::runtime_error("unexpected posix align"); } + +void Check(const char* input, int status, std::uint32_t expected) { + ++cases; + struct Output { std::uint32_t before; std::uint32_t value; std::uint32_t after; } output{0x12345678, 0xcafebeef, 0x87654321}; + const std::string saved = input ? input : ""; + const std::size_t oldAttempts = attempts; + const std::size_t oldReleases = releases; + observedOutput = &output.value; + expectedAtRelease = status == 0 ? expected : 0xcafebeef; + Require(tls_config_parse_protocols_nid_postfix(&output.value, input) == status); + observedOutput = nullptr; + Require(output.value == (status == 0 ? expected : 0xcafebeef)); + Require(output.before == 0x12345678 && output.after == 0x87654321); + Require(!input || saved == input); + Require(attempts == oldAttempts + (input ? 1 : 0)); + Require(releases == oldReleases + (input && !failAllocation ? 1 : 0)); + Require(live == nullptr); + if (input && !failAllocation) Require(liveSize == saved.size() + 1); +} + +void KnownCases() { + Check(nullptr, 0, 0x18); + const std::array, 24> good{{ + {"all", 0x18}, {"legacy", 0x18}, {"default", 0x18}, {"secure", 0x18}, {"tlsv1", 0x18}, + {"tlsv1.0", 2}, {"tlsv1.1", 4}, {"tlsv1.2", 8}, {"tlsv1.3", 16}, + {" \tTlSv1.2", 8}, {"!tlsv1.3", 8}, {"!tlsv1.2", 16}, {"!all", 0}, + {"tlsv1.0,tlsv1.1:tlsv1.2,tlsv1.3", 30}, {"all:!tlsv1.2", 16}, + {"all:!tlsv1.3", 8}, {"all,!all", 0}, {"!all,!tlsv1.2", 16}, + {"tlsv1.0,!tlsv1.0,!tlsv1.3", 8}, {"!all,!all", 0}, + {"tlsv1.0:!secure", 2}, {"!tlsv1.0", 24}, {"tlsv1.2,tlsv1.2", 8}, + {"\t!default:TLSV1.1", 4} + }}; + for (const auto& [input, expected] : good) Check(input, 0, expected); + const std::array bad{"", " ", "\t", "!", "!!all", "! all", "!\tall", "all ", "all\t", "all\n", "\nall", "\rall", "\vall", "\fall", "tlsv1.4", "tlsv1.20", "tlsv1.2x", "TLSv2", "sslv3", "none", "0x18", "*", ",all", "all,", "all::tlsv1.2", "all,,tlsv1.3", "all: \t", "all:bad", "tlsv1.2;tlsv1.3", "tlsv1.2 tlsv1.3", "all:!tlsv1.2:bad", "\xc2\xa0" "all"}; + for (const auto* input : bad) Check(input, -1, 0); + failAllocation = true; + Check(nullptr, 0, 0x18); + for (const auto& [input, expected] : good) Check(input, -1, 0); + for (const auto* input : bad) Check(input, -1, 0); + failAllocation = false; +} + +struct Term { std::string_view text; std::uint32_t bits; bool remove; }; +constexpr std::array Terms{{ + {"all",24,false},{"legacy",24,false},{"default",24,false},{"secure",24,false},{"tlsv1",24,false}, + {"tlsv1.0",2,false},{"tlsv1.1",4,false},{"tlsv1.2",8,false},{"tlsv1.3",16,false}, + {"!all",24,true},{"!legacy",24,true},{"!default",24,true},{"!secure",24,true},{"!tlsv1",24,true}, + {"!tlsv1.0",2,true},{"!tlsv1.1",4,true},{"!tlsv1.2",8,true},{"!tlsv1.3",16,true} +}}; + +std::uint32_t Expected(const std::array& terms) { + std::array enabled{}; + for (const auto& term : terms) { + if (term.remove && enabled == std::array{}) enabled = {false, false, true, true}; + for (std::size_t bit = 0; bit < enabled.size(); ++bit) { + if (term.bits & (2u << bit)) enabled[bit] = !term.remove; + } + } + std::uint32_t mask = 0; + for (std::size_t bit = 0; bit < enabled.size(); ++bit) if (enabled[bit]) mask += 2u << bit; + return mask; +} + +void Combinations() { + for (const auto& first : Terms) for (const auto& second : Terms) for (const auto& third : Terms) { + std::string input = std::string(first.text) + ", \t" + std::string(second.text) + ":\t" + std::string(third.text); + for (std::size_t i = 0; i < input.size(); ++i) if ((i & 1) && input[i] >= 'a' && input[i] <= 'z') input[i] -= 'a' - 'A'; + Check(input.c_str(), 0, Expected({first, second, third})); + } +} + +void CasePermutations() { + for (std::size_t term = 0; term < 9; ++term) { + std::string word(Terms[term].text); + for (unsigned mask = 0; mask < (1u << word.size()); ++mask) { + std::string input(word); + for (std::size_t index = 0; index < word.size(); ++index) if ((mask & (1u << index)) && word[index] >= 'a' && word[index] <= 'z') input[index] -= 'a' - 'A'; + Check(input.c_str(), 0, Terms[term].bits); + } + } + for (unsigned byte = 1; byte < 256; ++byte) { + std::string input(1, static_cast(byte)); + input += "all"; + if (byte == ' ' || byte == '\t') Check(input.c_str(), 0, 24); + else if (byte == '!') Check(input.c_str(), 0, 0); + else Check(input.c_str(), -1, 0); + } +} + +void Boundaries() { + std::string longList; + for (int index = 0; index < 1024; ++index) longList += "tlsv1.0,!tlsv1.0,"; + longList += "!tlsv1.2"; + Check(longList.c_str(), 0, 16); + longList += ",bad"; + Check(longList.c_str(), -1, 0); + const std::array embedded{'a','l','l','\0',',','b','a','d','\0','x'}; + Check(embedded.data(), 0, 24); + std::array alias{}; + auto* text = reinterpret_cast(alias.data()); + std::memcpy(text, "tlsv1.0,!tlsv1.0,!tlsv1.2", sizeof("tlsv1.0,!tlsv1.0,!tlsv1.2")); + const auto before = alias; + Require(tls_config_parse_protocols_nid_postfix(&alias[1], text) == 0); + Require(alias[1] == 16); + for (std::size_t i = 0; i < alias.size(); ++i) if (i != 1) Require(alias[i] == before[i]); + for (const auto* input : std::array{nullptr, "all", "bad"}) { + const auto oldAttempts = attempts; + bool caught = false; + try { tls_config_parse_protocols_nid_postfix(nullptr, input); } + catch (const std::invalid_argument&) { caught = true; } + Require(caught && attempts == oldAttempts && live == nullptr); + } +} + +} + +int main() { + std::array api{ + reinterpret_cast(Allocate_nid_no_patch), reinterpret_cast(Release_nid_no_patch), reinterpret_cast(UnexpectedCalloc_nid_no_patch), + reinterpret_cast(UnexpectedRealloc_nid_no_patch), reinterpret_cast(UnexpectedAlign_nid_no_patch), reinterpret_cast(UnexpectedRealign_nid_no_patch), + reinterpret_cast(UnexpectedPosixAlign_nid_no_patch)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + KnownCases(); + Combinations(); + CasePermutations(); + Boundaries(); + Require(live == nullptr); + std::printf("TLS protocol parser: %zu cases, %zu checks, %zu releases, zero live allocations\n", cases, checks, releases); +} diff --git a/docs/dev/debt/libSceLibreSsl3.md b/docs/dev/debt/libSceLibreSsl3.md index f2c05fc04b..1244db4196 100644 --- a/docs/dev/debt/libSceLibreSsl3.md +++ b/docs/dev/debt/libSceLibreSsl3.md @@ -38,11 +38,15 @@ - [ASN1_TIME_diff](../../../core/libs/prx/libSceLibreSsl3/src/Time.cpp) has no console ABI or behavior verification. Its signature and date rules follow LibreSSL 4.0.0 [a_time.c](https://github.com/libressl/openbsd/blob/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/asn1/a_time.c), [a_time_tm.c](https://github.com/libressl/openbsd/blob/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/asn1/a_time_tm.c) and [a_time_posix.c](https://github.com/libressl/openbsd/blob/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/asn1/a_time_posix.c), using the existing 24-byte string layout. Null inputs use the host wall clock; the console clock relationship is unverified. Null output pointers throw after valid input parsing, where the reference dereferences them. Closes when: measured on a PS5. +- [tls_config_parse_protocols](../../../core/libs/prx/libSceLibreSsl3/src/TlsProtocols.cpp) has no independent console execution or TLS configuration interoperability verification. Parsing follows [LibreSSL 4.0.0](https://github.com/libressl/openbsd/tree/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libtls). A null output pointer throws instead of an upstream fault. Tests use caller-owned text/output and guest-heap allocation failure; other TLS configuration and handshake exports remain unimplemented. Closes when: console grammar and TLS configuration interoperability are measured. + +- [Reported firmware 13.42 parser masks](../../../core/libs/prx/libSceLibreSsl3/src/TlsProtocols.cpp) come from a [contributor's disassembly](https://github.com/boykopovar/AnyPS5/pull/15045#pullrequestreview-5736573950) of `libSceLibreSsl3.sprx` (SHA-256 `ee2c529c87fe21b7ef1e5f13d10c47ee90944f52a1f848fe0995e031817c2c3a`), not independent execution here. The report identifies default/aggregate/negation-base mask `0x18` and explicit TLS 1.0/1.1/1.2/1.3 bits `2`/`4`/`8`/`16`, matching this implementation. Other firmware revisions, complete grammar and real TLS configuration use remain unverified. + - [EVP_PKEY_size, EVP_PKEY_security_bits and EVP_PKEY_cmp](../../../core/libs/prx/libSceLibreSsl3/src/PkeyQueries.cpp) have no console ABI or behavior verification. Dispatch follows [LibreSSL 4.0.0](https://github.com/libressl/openbsd/blob/ce488b78e0bd994780569df718596b9776ff681a/src/lib/libcrypto/evp/p_lib.c) using the existing 32-byte key and the LP64 method slots in that revision's `evp_local.h`: public comparison at 56, size at 96, security bits at 112 and parameter comparison at 160. Tests use caller-owned keys and callbacks; real key-constructor and algorithm-method interoperability remains unverified. Null comparison operands, undefined upstream, throw; null size/security queries and missing callbacks keep upstream's distinct results. Malformed method pointers remain unverified. Closes when: layouts and complete key lifetimes are measured on a console. ### Additional callable export inventory -- [Additional exports](../../../core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp) declare 166 remaining callable exports: 156 named and 10 known only by NID. Every entry throws. Parameterless `int` declarations are unknown-signature placeholders, not verified ABI contracts. Names/NIDs match the 13.60 export table. No functional implementation or title unblock is claimed. +- [Additional exports](../../../core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp) declare 165 remaining callable exports: 155 named and 10 known only by NID. Every entry throws. Parameterless `int` declarations are unknown-signature placeholders, not verified ABI contracts. Names/NIDs match the 13.60 export table. No functional implementation or title unblock is claimed. | Name | NID | | --- | --- | @@ -199,7 +203,6 @@ | `d2i_ASN1_SEQUENCE_ANY` | `vR0po3nBkBs` | | `PKCS5_PBKDF2_HMAC` | `vnwZHU5FvBQ` | | `ASN1_OCTET_STRING_free` | `vo-cv5gnPUA` | -| `tls_config_parse_protocols` | `vu4FgwGayHY` | | `SSL_CTX_set_post_handshake_auth` | `wHdymWL7CbE` | | `tls_config_insecure_noverifyname` | `wNQT6Zn4UG4` | | `X509_EXTENSION_free` | `wbJDMMsje6s` | @@ -213,7 +216,7 @@ | `ECDSA_SIG_get0` | `z8fsXF8JuL0` | | `EVP_MD_block_size` | `zCa75KLWWn4` | -- 107 of the 126 exports the 13.60 table lists only by NID carry, in the table above and in [Additional exports](../../../core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp), the LibreSSL names whose `tools/nid_names.py` hash equals the NID; the names were taken from the identifiers of the LibreSSL 3.8.4 and 4.0.0 `include`, `crypto`, `ssl` and `tls` sources. A name match does not show that the console function has LibreSSL's prototype or behavior; each keeps the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`. `AES_wrap_key`, `ECDSA_SIG_free`, `ECDSA_do_verify`, `ERR_load_ERR_strings`, `ERR_print_errors_fp` and `i2d_PUBKEY` also hash to the NIDs of six `unknown` rows, which keep their NID names; `PBYz0kpXwg8` hashes to no identifier of those sources. +- 106 of the 126 exports the 13.60 table lists only by NID carry, in the table above and in [Additional exports](../../../core/libs/prx/libSceLibreSsl3/AdditionalExports.cpp), the LibreSSL names whose `tools/nid_names.py` hash equals the NID; the names were taken from the identifiers of the LibreSSL 3.8.4 and 4.0.0 `include`, `crypto`, `ssl` and `tls` sources. A name match does not show that the console function has LibreSSL's prototype or behavior; each keeps the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`. `AES_wrap_key`, `ECDSA_SIG_free`, `ECDSA_do_verify`, `ERR_load_ERR_strings`, `ERR_print_errors_fp` and `i2d_PUBKEY` also hash to the NIDs of six `unknown` rows, which keep their NID names; `PBYz0kpXwg8` hashes to no identifier of those sources. ### Functional