diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index f22572c2ed..e4635639c7 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -106,6 +106,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_libressl_general_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslGeneralName.cpp) + target_include_directories(guest_libressl_general_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_general_name_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_general_name_tests) + add_test(NAME guest_libressl_general_name COMMAND guest_libressl_general_name_tests) + set_tests_properties(guest_libressl_general_name PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_general_name PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_libressl_ui_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslUi.cpp) target_include_directories(guest_libressl_ui_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libressl_ui_tests PRIVATE libSceLibreSsl libc) @@ -253,6 +264,7 @@ if(BUILD_TESTING) endif() target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/X509VerifyParam.cpp) +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/GeneralName.cpp) if(BUILD_TESTING) add_test_executable(guest_libressl_x509_verify_param_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/X509VerifyParam.cpp) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 78d3b801d2..fc78f64744 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -719,16 +719,6 @@ int APS5_VABI EVP_sha512_nid_postfix(void) { return 0; } -int APS5_VABI GENERAL_NAMES_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI GENERAL_NAME_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI HMAC_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index 7e8b8b4d10..3e3d9b7601 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -9,6 +9,11 @@ struct Asn1String { std::int64_t flags; }; +struct LibreSslGeneralName { + std::int32_t type; + void* value; +}; + struct LibreSslX509VerifyParamId { void* hosts; std::uint32_t hostFlags; @@ -395,8 +400,8 @@ int APS5_VABI EVP_md5_nid_postfix(void); int APS5_VABI EVP_sha256_nid_postfix(void); int APS5_VABI EVP_sha384_nid_postfix(void); int APS5_VABI EVP_sha512_nid_postfix(void); -int APS5_VABI GENERAL_NAMES_free_nid_postfix(void); -int APS5_VABI GENERAL_NAME_free_nid_postfix(void); +void APS5_VABI GENERAL_NAMES_free_nid_postfix(LibreSslStack* names); +void APS5_VABI GENERAL_NAME_free_nid_postfix(LibreSslGeneralName* name); int APS5_VABI HMAC_nid_postfix(void); int APS5_VABI HMAC_CTX_cleanup_nid_postfix(void); int APS5_VABI HMAC_CTX_init_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/GeneralName.cpp b/core/libs/prx/libSceLibreSsl/src/GeneralName.cpp new file mode 100644 index 0000000000..6c9d5bcabb --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/GeneralName.cpp @@ -0,0 +1,185 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include +#include +#include + +static_assert(sizeof(LibreSslGeneralName) == 16); +static_assert(offsetof(LibreSslGeneralName, value) == 8); +static_assert(sizeof(Asn1String) == 24); +static_assert(sizeof(LibreSslAsn1Object) == 40); + +namespace { + +constexpr std::int64_t Asn1StringNdef = 0x10; +constexpr int Asn1Boolean = 1; +constexpr int Asn1Null = 5; +constexpr int Asn1Object = 6; +constexpr int Asn1Any = -4; +constexpr int DynamicObject = 0x01; +constexpr int DynamicStrings = 0x04; +constexpr int DynamicData = 0x08; + +struct Asn1Type { + int type; + void* value; +}; + +struct OtherName { + LibreSslAsn1Object* typeId; + Asn1Type* value; +}; + +struct EdiPartyName { + Asn1String* nameAssigner; + Asn1String* partyName; +}; + +struct Buffer { + std::size_t length; + char* data; + std::size_t capacity; +}; + +struct X509NameEntry { + LibreSslAsn1Object* object; + Asn1String* value; + int set; + int size; +}; + +struct X509Name { + LibreSslStack* entries; + int modified; + Buffer* bytes; + unsigned char* canonical; + int canonicalLength; +}; + +static_assert(sizeof(Asn1Type) == 16 && offsetof(Asn1Type, value) == 8); +static_assert(sizeof(OtherName) == 16 && sizeof(EdiPartyName) == 16); +static_assert(sizeof(Buffer) == 24 && offsetof(Buffer, capacity) == 16); +static_assert(sizeof(X509NameEntry) == 24 && offsetof(X509NameEntry, set) == 16); +static_assert(sizeof(X509Name) == 40 && offsetof(X509Name, bytes) == 16); +static_assert(offsetof(X509Name, canonical) == 24); + +void Wipe(void* pointer, std::size_t size) { + auto* bytes = static_cast(pointer); + for (std::size_t i = 0; i < size; ++i) bytes[i] = 0; +} + +void FreeString(Asn1String* string) { + if (!string) return; + if (string->data && !(string->flags & Asn1StringNdef)) { + if (string->length < 0) throw std::invalid_argument("ASN1_STRING_free: negative length"); + Wipe(string->data, static_cast(string->length)); + free_nid_postfix(string->data); + } + free_nid_postfix(string); +} + +void FreeObject(LibreSslAsn1Object* object) { + if (!object) return; + if (object->flags & DynamicStrings) { + free_nid_postfix(const_cast(object->shortName)); + free_nid_postfix(const_cast(object->longName)); + object->shortName = nullptr; + object->longName = nullptr; + } + if (object->flags & DynamicData) { + if (object->data) { + if (object->length < 0) throw std::invalid_argument("ASN1_OBJECT_free: negative length"); + Wipe(const_cast(object->data), static_cast(object->length)); + } + free_nid_postfix(const_cast(object->data)); + object->data = nullptr; + object->length = 0; + } + if (object->flags & DynamicObject) free_nid_postfix(object); +} + +void FreeAny(Asn1Type* any) { + if (!any) return; + if (any->value) { + if (any->type == Asn1Object) FreeObject(static_cast(any->value)); + else if (any->type == Asn1Any) FreeAny(static_cast(any->value)); + else if (any->type != Asn1Boolean && any->type != Asn1Null) FreeString(static_cast(any->value)); + } + free_nid_postfix(any); +} + +void FreeBuffer(Buffer* buffer) { + if (!buffer) return; + if (buffer->data) { + Wipe(buffer->data, buffer->capacity); + free_nid_postfix(buffer->data); + } + free_nid_postfix(buffer); +} + +void FreeX509Name(X509Name* name) { + if (!name) return; + FreeBuffer(name->bytes); + sk_pop_free_nid_postfix(name->entries, +[](void* pointer) APS5_VABI { + auto* entry = static_cast(pointer); + if (!entry) return; + + FreeString(entry->value); + FreeObject(entry->object); + free_nid_postfix(entry); + }); + free_nid_postfix(name->canonical); + free_nid_postfix(name); +} + +} + +extern "C" { + +void APS5_VABI GENERAL_NAME_free_nid_postfix(LibreSslGeneralName* name) { + if (!name) return; + switch (name->type) { + case 0: { + auto* other = static_cast(name->value); + if (other) { + FreeAny(other->value); + FreeObject(other->typeId); + free_nid_postfix(other); + } + break; + } + case 1: + case 2: + case 3: + case 6: + case 7: + FreeString(static_cast(name->value)); + break; + case 4: + FreeX509Name(static_cast(name->value)); + break; + case 5: { + auto* edi = static_cast(name->value); + if (edi) { + FreeString(edi->partyName); + FreeString(edi->nameAssigner); + free_nid_postfix(edi); + } + break; + } + case 8: + FreeObject(static_cast(name->value)); + break; + default: + break; + } + free_nid_postfix(name); +} + +void APS5_VABI GENERAL_NAMES_free_nid_postfix(LibreSslStack* names) { + sk_pop_free_nid_postfix(names, +[](void* pointer) APS5_VABI { + GENERAL_NAME_free_nid_postfix(static_cast(pointer)); + }); +} + +} diff --git a/core/libs/tests/GuestLibreSslGeneralName.cpp b/core/libs/tests/GuestLibreSslGeneralName.cpp new file mode 100644 index 0000000000..ccc3e2093a --- /dev/null +++ b/core/libs/tests/GuestLibreSslGeneralName.cpp @@ -0,0 +1,266 @@ +#include "prx/libSceLibreSsl/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using FreeName = void (APS5_VABI*)(LibreSslGeneralName*); +using FreeNames = void (APS5_VABI*)(LibreSslStack*); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(sizeof(LibreSslGeneralName) == 16); + +namespace { + +void Require(bool success, std::source_location where = std::source_location::current()) { + if (success) return; + std::fprintf(stderr, "GENERAL_NAME free test failed at line %u\n", where.line()); + std::abort(); +} + +std::unordered_map live; +std::unordered_map wipes; +std::vector freed; + +void* APS5_VABI Allocate(std::size_t bytes) { + void* pointer = std::malloc(bytes); + if (pointer) live.emplace(pointer, bytes); + return pointer; +} + +void APS5_VABI Free(void* pointer) { + if (!pointer) return; + Require(live.erase(pointer) == 1); + const auto it = wipes.find(pointer); + if (it != wipes.end()) { + const auto* bytes = static_cast(pointer); + for (std::size_t i = 0; i < it->second; ++i) Require(bytes[i] == 0); + wipes.erase(it); + } + freed.push_back(pointer); + std::free(pointer); +} + +void* APS5_VABI NoCalloc(std::size_t, std::size_t) { throw std::runtime_error("unexpected calloc"); } +void* APS5_VABI NoRealloc(void*, std::size_t) { throw std::runtime_error("unexpected realloc"); } +void* APS5_VABI NoAlign(std::size_t, std::size_t) { throw std::runtime_error("unexpected align"); } +void* APS5_VABI NoRealign(void*, std::size_t, std::size_t) { throw std::runtime_error("unexpected realign"); } +int APS5_VABI NoPosixAlign(void**, std::size_t, std::size_t) { throw std::runtime_error("unexpected posix_memalign"); } + +void InitializeHeap() { + void* const api[10] {reinterpret_cast(Allocate), reinterpret_cast(Free), + reinterpret_cast(NoCalloc), reinterpret_cast(NoRealloc), + reinterpret_cast(NoAlign), reinterpret_cast(NoRealign), + reinterpret_cast(NoPosixAlign)}; + ApplicationHeapRegister_nid_no_patch(api); +} + +template T* New() { + auto* pointer = static_cast(malloc_nid_postfix(sizeof(T))); + Require(pointer != nullptr); + std::memset(pointer, 0, sizeof(T)); + return pointer; +} + +unsigned char* Bytes(std::size_t size, std::size_t wipeLength = 0) { + auto* pointer = static_cast(malloc_nid_postfix(size)); + Require(pointer != nullptr); + std::memset(pointer, 0xa5, size); + if (wipeLength) wipes.emplace(pointer, wipeLength); + return pointer; +} + +Asn1String* String(int size = 7, int flags = 0) { + auto* result = New(); + result->length = size; + result->type = 22; + result->flags = flags; + if (size > 0 && !(flags & 0x10)) result->data = Bytes(static_cast(size), static_cast(size)); + return result; +} + +LibreSslAsn1Object* Object(int flags = 0x0d) { + auto* object = New(); + object->flags = flags; + if (flags & 0x04) { + object->shortName = reinterpret_cast(Bytes(6)); + object->longName = reinterpret_cast(Bytes(9)); + } + if (flags & 0x08) { + object->length = 5; + object->data = Bytes(5, 5); + } + return object; +} + +LibreSslGeneralName* Name(int type, void* value) { + auto* result = New(); + result->type = type; + result->value = value; + return result; +} + +struct Any { int type; void* value; }; +struct Other { LibreSslAsn1Object* typeId; Any* value; }; +struct Edi { Asn1String* assigner; Asn1String* party; }; +struct Buffer { std::size_t length; char* data; std::size_t capacity; }; +struct NameEntry { LibreSslAsn1Object* object; Asn1String* value; int set; int size; }; +struct X509Name { LibreSslStack* entries; int modified; Buffer* bytes; unsigned char* canonical; int canonicalLength; }; + +static_assert(sizeof(Any) == 16 && sizeof(Other) == 16 && sizeof(Edi) == 16); +static_assert(sizeof(Buffer) == 24 && sizeof(NameEntry) == 24 && sizeof(X509Name) == 40); + +LibreSslStack* Stack(const std::vector& elements) { + auto* stack = New(); + stack->num = static_cast(elements.size()); + stack->numAlloc = stack->num; + stack->data = reinterpret_cast(malloc_nid_postfix(sizeof(void*) * (elements.size() ? elements.size() : 1))); + Require(stack->data != nullptr); + for (std::size_t i = 0; i < elements.size(); ++i) stack->data[i] = static_cast(elements[i]); + return stack; +} + +void CheckClean() { + Require(live.empty()); + Require(wipes.empty()); +} + +void TestSimpleNames() { + GENERAL_NAME_free_nid_postfix(nullptr); + GENERAL_NAMES_free_nid_postfix(nullptr); + for (int choice : {1, 2, 3, 6, 7}) { + GENERAL_NAME_free_nid_postfix(Name(choice, String())); + CheckClean(); + } + auto* string = String(0); + GENERAL_NAME_free_nid_postfix(Name(1, string)); + CheckClean(); + + std::array borrowed{1, 2, 3, 4, 5, 6}; + string = String(0, 0x10); + string->length = static_cast(borrowed.size()); + string->data = borrowed.data(); + GENERAL_NAME_free_nid_postfix(Name(2, string)); + Require(borrowed == (std::array{1, 2, 3, 4, 5, 6})); + CheckClean(); +} + +void TestOtherName() { + for (int type : {1, 5, 6, 12, -4}) { + auto* other = New(); + other->typeId = Object(); + other->value = New(); + other->value->type = type; + if (type == 6) other->value->value = Object(0x09); + if (type == 12) other->value->value = String(); + if (type == -4) { + auto* nested = New(); + nested->type = 12; + nested->value = String(); + other->value->value = nested; + } + GENERAL_NAME_free_nid_postfix(Name(0, other)); + CheckClean(); + } + + auto* other = New(); + static LibreSslAsn1Object staticOid{}; + other->typeId = &staticOid; + GENERAL_NAME_free_nid_postfix(Name(0, other)); + CheckClean(); +} + +void TestEdiPartyName() { + auto* edi = New(); + edi->assigner = String(); + edi->party = String(); + GENERAL_NAME_free_nid_postfix(Name(5, edi)); + CheckClean(); + edi = New(); + edi->party = String(); + GENERAL_NAME_free_nid_postfix(Name(5, edi)); + CheckClean(); +} + +void TestDirectoryName() { + auto* name = New(); + auto* buffer = New(); + buffer->capacity = 16; + buffer->length = 4; + buffer->data = reinterpret_cast(Bytes(16, 16)); + name->bytes = buffer; + auto* entry = New(); + entry->object = Object(); + entry->value = String(); + auto* another = New(); + another->object = Object(0x01); + another->value = String(); + name->entries = Stack({entry, nullptr, another}); + name->canonical = Bytes(12); + name->canonicalLength = 12; + GENERAL_NAME_free_nid_postfix(Name(4, name)); + CheckClean(); +} + +void TestOidAndStack() { + GENERAL_NAME_free_nid_postfix(Name(8, Object())); + CheckClean(); + + static LibreSslAsn1Object staticOid{}; + staticOid.length = 5; + staticOid.flags = 0x08; + staticOid.data = Bytes(5, 5); + GENERAL_NAME_free_nid_postfix(Name(8, &staticOid)); + Require(staticOid.data == nullptr); + Require(staticOid.length == 0); + GENERAL_NAME_free_nid_postfix(Name(8, &staticOid)); + CheckClean(); + + static LibreSslAsn1Object staticNamedOid{}; + staticNamedOid.flags = 0x0c; + staticNamedOid.shortName = reinterpret_cast(Bytes(6)); + staticNamedOid.longName = reinterpret_cast(Bytes(9)); + staticNamedOid.length = 7; + staticNamedOid.data = Bytes(7, 7); + GENERAL_NAME_free_nid_postfix(Name(8, &staticNamedOid)); + Require(staticNamedOid.shortName == nullptr); + Require(staticNamedOid.longName == nullptr); + Require(staticNamedOid.data == nullptr); + Require(staticNamedOid.length == 0); + GENERAL_NAME_free_nid_postfix(Name(8, &staticNamedOid)); + CheckClean(); + + auto* first = Name(1, String()); + auto* last = Name(7, String()); + freed.clear(); + GENERAL_NAMES_free_nid_postfix(Stack({first, nullptr, last})); + CheckClean(); + Require(freed.size() >= 8); + GENERAL_NAMES_free_nid_postfix(Stack({})); + CheckClean(); + + auto* unusual = Name(42, nullptr); + GENERAL_NAME_free_nid_postfix(unusual); + CheckClean(); +} + +} + +int main() { + InitializeHeap(); + TestSimpleNames(); + TestOtherName(); + TestEdiPartyName(); + TestDirectoryName(); + TestOidAndStack(); + std::puts("libSceLibreSsl GENERAL_NAME tests passed"); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 99dd5f9578..ad3e0e8359 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -4,6 +4,7 @@ - [SSL_select_next_proto](../../../core/libs/prx/libSceLibreSsl/src/ProtocolSelection.cpp) (libSceLibreSsl) follows LibreSSL 3.4.1 [`ssl_lib.c`](https://github.com/libressl/openbsd/blob/d7513e7d4daf94905fd4cb0a5e5c89109d2874f7/src/lib/libssl/ssl_lib.c), which validates neither list: a zero-length entry is negotiated, no overlap returns `supported + 1` with the unchecked length byte `supported[0]`, read even for an empty supported list, and each pair of entries with equal length bytes is compared over that many bytes, past the end of a truncated list. [Firmware 13.42 host-PC evidence](https://github.com/boykopovar/AnyPS5/pull/9785#issuecomment-6406712390) of `libSceLibreSsl.sprx` (`0xda6d0`) reports the same status, pointer and length for 700,000 valid pairs and for the six unvalidated cases. Null outputs, a null supported list and a null peer list with a non-zero length throw, where the module faults; nothing is measured on a console. - [SHA384_Init and SHA512_Init](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - clear the 128-byte block buffer of the context, which LibreSSL 2.5.4 leaves as it was; digests are unaffected. `guest_libressl_oracle` (`ANYPS5_LIBRESSL_ORACLE`) compares the rest of the context with LibreSSL 2.5.4 +- [GENERAL_NAME_free and GENERAL_NAMES_free](../../../core/libs/prx/libSceLibreSsl/src/GeneralName.cpp) (libSceLibreSsl) - release all nine GENERAL_NAME choices and STACK_OF(GENERAL_NAME) using the guest allocator, following [LibreSSL 2.5.4 `v3_genn.c`](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509v3/v3_genn.c) and its [ASN.1 free dispatcher](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1/tasn_fre.c). ASN1_STRING and dynamically allocated OID contents are wiped before freeing; NDEF strings and static OIDs retain their borrowed memory. Nested X509_NAME entries and cached data are freed as in 2.5.4. Guest allocator tests check owned/borrowed memory and disposal of nested allocations. PS5 native module behavior and malformed-memory failure cases are not verified; negative owned data lengths throw rather than risk out-of-bounds memory erasure. ### Unknown function info