Merge pull request #15045 from atharvjairath/feat/libressl3-tls-protocol-parser

feat(libSceLibreSsl3): implement TLS protocol list parsing
This commit is contained in:
Dean authored and GitHub committed 2026-11-06 19:47:27 +13:00
commit bbd85f7ebf
6 files changed
+280 -8

No files matched your search

@@ -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__);
}
@@ -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:$<TARGET_FILE_DIR:libSceLibreSsl3>")
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)
@@ -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();
@@ -0,0 +1,70 @@
#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp"
#include "prx/libc/libc.hpp"
#include <array>
#include <cstdint>
#include <stdexcept>
#include <string_view>
#include <utility>
namespace {
constexpr std::uint32_t SupportedProtocols = 0x18;
constexpr std::array<std::pair<std::string_view, std::uint32_t>, 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<unsigned char>(text[index]);
const unsigned char folded = byte >= 'A' && byte <= 'Z' ? byte + ('a' - 'A') : byte;
if (folded != static_cast<unsigned char>(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;
}
@@ -0,0 +1,189 @@
#include "prx/libSceLibreSsl3/libSceLibreSsl3.hpp"
#include "prx/libc/include/ApplicationHeap.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <source_location>
#include <stdexcept>
#include <string>
#include <string_view>
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<unsigned char*>(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<unsigned char*>(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<std::pair<const char*, std::uint32_t>, 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<Term, 18> 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<Term, 3>& terms) {
std::array<bool, 4> enabled{};
for (const auto& term : terms) {
if (term.remove && enabled == std::array<bool, 4>{}) 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<char>(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<char, 10> embedded{'a','l','l','\0',',','b','a','d','\0','x'};
Check(embedded.data(), 0, 24);
std::array<std::uint32_t, 8> alias{};
auto* text = reinterpret_cast<char*>(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<const char*, 3>{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<void*, 10> api{
reinterpret_cast<void*>(Allocate_nid_no_patch), reinterpret_cast<void*>(Release_nid_no_patch), reinterpret_cast<void*>(UnexpectedCalloc_nid_no_patch),
reinterpret_cast<void*>(UnexpectedRealloc_nid_no_patch), reinterpret_cast<void*>(UnexpectedAlign_nid_no_patch), reinterpret_cast<void*>(UnexpectedRealign_nid_no_patch),
reinterpret_cast<void*>(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);
}
+6 -3
View File
@@ -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