diff --git a/DEPENDENCIES b/DEPENDENCIES index 7842dff4..3a23e029 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core 04e936961d1e31f6b70fa5a30e115b2f7855674a -jsonbinpack https://github.com/sourcemeta/jsonbinpack ac8e1af733a781fc4c94a14157f80970ea569479 -blaze https://github.com/sourcemeta/blaze e73e5e1667487717b44ab351a0f46e01d507fefb +core https://github.com/sourcemeta/core bb1c78e8fa148a2ece951bb776798a43fe328821 +jsonbinpack https://github.com/sourcemeta/jsonbinpack e2f99ed5e69ab17b027c3d7bb0ef95b27953bb08 +blaze https://github.com/sourcemeta/blaze 04832d45bf4327d4ec874fa67f339797cd49b375 ctrf https://github.com/ctrf-io/ctrf 93ea827d951390190171d37443bff169cf47c808 diff --git a/README.markdown b/README.markdown index e80804ad..ba6a4934 100644 --- a/README.markdown +++ b/README.markdown @@ -207,7 +207,7 @@ to build from source if your distribution of choice is different. > missing you can install it through your package manager (for example > `apt install libcurl4`, `dnf install libcurl`, or `apk add libcurl`). If it > lives in a non-standard location, point the CLI at it by setting the -> `SOURCEMETA_JSONSCHEMA_CURL_SO` environment variable to the full path of +> `SOURCEMETA_CORE_CURL_SO` environment variable to the full path of > `libcurl.so.4`. To verify the GPG signature of the checksums file: diff --git a/cmake/FindCore.cmake b/cmake/FindCore.cmake index 4a5a71d5..cec4463e 100644 --- a/cmake/FindCore.cmake +++ b/cmake/FindCore.cmake @@ -6,6 +6,9 @@ if(NOT Core_FOUND) set(SOURCEMETA_CORE_LANG_PARALLEL OFF CACHE BOOL "Parallel") set(SOURCEMETA_CORE_LANG_STACKTRACE OFF CACHE BOOL "Stacktrace") set(SOURCEMETA_CORE_HTML OFF CACHE BOOL "HTML") + if(JSONSCHEMA_USE_SYSTEM_CURL) + set(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL ON CACHE BOOL "Use system cURL") + endif() add_subdirectory("${PROJECT_SOURCE_DIR}/vendor/core") include(Sourcemeta) set(Core_FOUND ON) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2f6ffaa7..de9beb66 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,3 @@ -add_subdirectory(http) - sourcemeta_executable( PROJECT jsonschema NAME cli @@ -46,7 +44,6 @@ target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::output) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::test) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::alterschema) target_link_libraries(jsonschema_cli PRIVATE sourcemeta::blaze::codegen) -target_link_libraries(jsonschema_cli PRIVATE jsonschema_http) configure_file(configure.h.in configure.h @ONLY) target_include_directories(jsonschema_cli PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/src/error.h b/src/error.h index 0fe80176..d9ba3afe 100644 --- a/src/error.h +++ b/src/error.h @@ -28,7 +28,6 @@ #include // std::vector #include "exit_code.h" -#include "http.h" // sourcemeta::jsonschema::HTTPDynamicBackendNotFound namespace sourcemeta::jsonschema { @@ -1099,7 +1098,7 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_UNEXPECTED_ERROR; - } catch (const sourcemeta::jsonschema::HTTPDynamicBackendNotFound &error) { + } catch (const sourcemeta::core::HTTPSystemBackendError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_NOT_SUPPORTED; diff --git a/src/http/CMakeLists.txt b/src/http/CMakeLists.txt deleted file mode 100644 index 64e1d32d..00000000 --- a/src/http/CMakeLists.txt +++ /dev/null @@ -1,25 +0,0 @@ -if(APPLE) - add_library(jsonschema_http STATIC http.h http_darwin.mm) - set_source_files_properties(http_darwin.mm - PROPERTIES COMPILE_OPTIONS "-fobjc-arc") - target_link_libraries(jsonschema_http PRIVATE "-framework Foundation") - target_link_libraries(jsonschema_http PRIVATE sourcemeta::core::text) -elseif(WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "MSYS") - add_library(jsonschema_http STATIC http.h http_windows.cc) - target_link_libraries(jsonschema_http PRIVATE winhttp) - target_link_libraries(jsonschema_http PRIVATE sourcemeta::core::unicode) -else() - add_library(jsonschema_http STATIC http.h http_curl.cc) - if(JSONSCHEMA_USE_SYSTEM_CURL) - find_package(CURL REQUIRED) - target_compile_definitions(jsonschema_http PRIVATE JSONSCHEMA_USE_SYSTEM_CURL) - target_link_libraries(jsonschema_http PRIVATE CURL::libcurl) - else() - target_link_libraries(jsonschema_http PRIVATE ${CMAKE_DL_LIBS}) - endif() -endif() - -sourcemeta_add_default_options(PRIVATE jsonschema_http) -target_link_libraries(jsonschema_http PUBLIC sourcemeta::core::http) -target_include_directories(jsonschema_http PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") -set_target_properties(jsonschema_http PROPERTIES FOLDER "jsonschema/http") diff --git a/src/http/http.h b/src/http/http.h deleted file mode 100644 index c4c6aed5..00000000 --- a/src/http/http.h +++ /dev/null @@ -1,86 +0,0 @@ -#ifndef SOURCEMETA_JSONSCHEMA_CLI_HTTP_H_ -#define SOURCEMETA_JSONSCHEMA_CLI_HTTP_H_ - -#include - -#include // std::size_t -#include // std::exception -#include // std::optional -#include // std::string -#include // std::string_view -#include // std::pair, std::move -#include // std::vector - -namespace sourcemeta::jsonschema { - -inline constexpr std::string_view HTTP_RESPONSE_TOO_LARGE_MESSAGE{ - "The response exceeds the maximum allowed size"}; - -struct HTTPRequestBody { - // Sent as the `Content-Type` header of the request, so do not also - // set that header by hand - std::string_view content_type; - std::string_view data; -}; - -struct HTTPRequest { - sourcemeta::core::HTTPMethod method{sourcemeta::core::HTTPMethod::GET}; - // All request fields are views: the caller must keep the data they - // refer to alive until `http_request` returns - std::string_view url; - // Repeated header names are permitted. Note that some platform HTTP - // stacks fold them into a single comma-separated field line, which is - // semantically equivalent per RFC 9110 - std::vector> headers; - std::optional body; - // When set, abort with an `HTTPError` if the response body exceeds - // this number of bytes - std::optional maximum_response_size; -}; - -struct HTTPResponse { - sourcemeta::core::HTTPStatus status{}; - // Header names are normalised to lowercase and repeated headers are - // preserved as separate entries, except on platform HTTP stacks like - // NSURLSession that fold them into a single comma-separated entry. The - // response owns its data, as the backend buffers it was read from do - // not outlive the request - std::vector> headers; - std::string body; -}; - -class HTTPDynamicBackendNotFound : public std::exception { -public: - HTTPDynamicBackendNotFound(std::string message, std::string variable, - std::vector paths) - : message_{std::move(message)}, variable_{std::move(variable)}, - paths_{std::move(paths)} {} - - [[nodiscard]] auto what() const noexcept -> const char * override { - return this->message_.c_str(); - } - - [[nodiscard]] auto variable() const noexcept -> const std::string & { - return this->variable_; - } - - [[nodiscard]] auto paths() const noexcept - -> const std::vector & { - return this->paths_; - } - -private: - std::string message_; - std::string variable_; - std::vector paths_; -}; - -// Perform an HTTP request, following redirects. This function is -// implemented on top of NSURLSession on Apple platforms, WinHTTP on -// Windows, and cURL everywhere else. Failures to obtain a response are -// reported as `sourcemeta::core::HTTPError` exceptions -auto http_request(const HTTPRequest &request) -> HTTPResponse; - -} // namespace sourcemeta::jsonschema - -#endif diff --git a/src/resolver.h b/src/resolver.h index 46ddd165..c5684bd1 100644 --- a/src/resolver.h +++ b/src/resolver.h @@ -12,7 +12,6 @@ #include #include "error.h" -#include "http.h" #include "input.h" #include "logger.h" #include "utils.h" @@ -135,19 +134,19 @@ collect_http_headers(const sourcemeta::core::Options &options) static inline auto http_fetch(const std::string &url, const sourcemeta::core::Options &options) -> sourcemeta::core::JSON { - const HTTPRequest request{.method = sourcemeta::core::HTTPMethod::GET, - .url = url, - .headers = collect_http_headers(options), - .body = std::nullopt, - .maximum_response_size = std::nullopt}; - HTTPResponse response; + sourcemeta::core::HTTPSystemRequest request{url}; + for (const auto &header : collect_http_headers(options)) { + request.header(std::string{header.first}, std::string{header.second}); + } + + sourcemeta::core::HTTPResponse response; for (std::uint8_t attempt{1}; attempt <= HTTP_MAXIMUM_RETRIES; ++attempt) { LOG_VERBOSE(options) << "Resolving over HTTP (attempt " << static_cast(attempt) << "/" << static_cast(HTTP_MAXIMUM_RETRIES) << "): " << url << "\n"; try { - response = http_request(request); + response = request.send(); } catch (const sourcemeta::core::HTTPError &error) { if (attempt == HTTP_MAXIMUM_RETRIES) { throw; @@ -171,8 +170,8 @@ static inline auto http_fetch(const std::string &url, } if (response.status != sourcemeta::core::HTTP_STATUS_OK) { - throw sourcemeta::core::HTTPStatusError{request.method, url, - response.status}; + throw sourcemeta::core::HTTPStatusError{sourcemeta::core::HTTPMethod::GET, + url, response.status}; } const auto content_type{ diff --git a/test/ci/fail_validate_http_missing_curl.sh b/test/ci/fail_validate_http_missing_curl.sh index 16a2ccbe..01348fa6 100755 --- a/test/ci/fail_validate_http_missing_curl.sh +++ b/test/ci/fail_validate_http_missing_curl.sh @@ -20,7 +20,7 @@ cat << 'EOF' > "$TMP/instance.json" { "type": "string" } EOF -SOURCEMETA_JSONSCHEMA_CURL_SO="$TMP/does-not-exist.so" \ +SOURCEMETA_CORE_CURL_SO="$TMP/does-not-exist.so" \ "$1" validate "$TMP/schema.json" "$TMP/instance.json" --http \ > "$TMP/stdout.txt" 2> "$TMP/stderr.txt" \ && EXIT_CODE="$?" || EXIT_CODE="$?" @@ -29,14 +29,14 @@ test "$EXIT_CODE" = "3" cat << EOF > "$TMP/expected.txt" error: Could not load the cURL library from the configured path - with environment variable SOURCEMETA_JSONSCHEMA_CURL_SO + with environment variable SOURCEMETA_CORE_CURL_SO with paths - $TMP/does-not-exist.so EOF diff "$TMP/stderr.txt" "$TMP/expected.txt" -SOURCEMETA_JSONSCHEMA_CURL_SO="$TMP/does-not-exist.so" \ +SOURCEMETA_CORE_CURL_SO="$TMP/does-not-exist.so" \ "$1" validate "$TMP/schema.json" "$TMP/instance.json" --http --json \ > "$TMP/stdout_json.txt" 2> "$TMP/stderr_json.txt" \ && EXIT_CODE="$?" || EXIT_CODE="$?" @@ -46,7 +46,7 @@ test "$EXIT_CODE" = "3" cat << EOF > "$TMP/expected_json.txt" { "error": "Could not load the cURL library from the configured path", - "environmentVariable": "SOURCEMETA_JSONSCHEMA_CURL_SO", + "environmentVariable": "SOURCEMETA_CORE_CURL_SO", "paths": [ "$TMP/does-not-exist.so" ] } EOF diff --git a/vendor/blaze/DEPENDENCIES b/vendor/blaze/DEPENDENCIES index 2bf88c51..a7156988 100644 --- a/vendor/blaze/DEPENDENCIES +++ b/vendor/blaze/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core df8f2970ccf85a3a3f01e004ac436ff916f8c52a +core https://github.com/sourcemeta/core bb1c78e8fa148a2ece951bb776798a43fe328821 jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite 60755c1097769e313fae3ec4d63bcc9d49b5d2d5 jsonschema-2020-12 https://github.com/json-schema-org/json-schema-spec 769daad75a9553562333a8937a187741cb708c72 jsonschema-2019-09 https://github.com/json-schema-org/json-schema-spec 41014ea723120ce70b314d72f863c6929d9f3cfd diff --git a/vendor/blaze/schemas/canonical-draft3.json b/vendor/blaze/schemas/canonical-draft3.json index 9089bf0c..f9a35075 100644 --- a/vendor/blaze/schemas/canonical-draft3.json +++ b/vendor/blaze/schemas/canonical-draft3.json @@ -137,9 +137,6 @@ }, "format": { "type": "string" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -169,9 +166,6 @@ }, "maximum": { "type": "number" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -207,9 +201,6 @@ }, "exclusiveMaximum": { "type": "boolean" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -256,9 +247,6 @@ "type": "boolean" } ] - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -292,9 +280,6 @@ }, "uniqueItems": { "type": "boolean" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -348,9 +333,6 @@ }, "uniqueItems": { "type": "boolean" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -397,6 +379,9 @@ "items": { "$ref": "#/$defs/schema" } + }, + "required": { + "type": "boolean" } }, "unevaluatedProperties": false @@ -416,6 +401,7 @@ "properties": { "disallow": { "type": "array", + "maxItems": 1, "minItems": 1, "items": { "$ref": "#/$defs/schema" diff --git a/vendor/blaze/src/alterschema/CMakeLists.txt b/vendor/blaze/src/alterschema/CMakeLists.txt index 37c5b5a1..6fd7ad96 100644 --- a/vendor/blaze/src/alterschema/CMakeLists.txt +++ b/vendor/blaze/src/alterschema/CMakeLists.txt @@ -12,13 +12,18 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema canonicalizer/dependent_schemas_to_any_of.h canonicalizer/deprecated_false_drop.h canonicalizer/draft3_type_any.h + canonicalizer/disallow_array_to_extends.h + canonicalizer/disallow_extends_to_type.h canonicalizer/disallow_to_array_of_schemas.h + canonicalizer/disallow_type_union_to_extends.h canonicalizer/divisible_by_implicit.h + canonicalizer/duplicate_disallow_entries.h canonicalizer/empty_definitions_drop.h canonicalizer/empty_defs_drop.h canonicalizer/empty_dependencies_drop.h canonicalizer/empty_dependent_required_drop.h canonicalizer/empty_dependent_schemas_drop.h + canonicalizer/empty_disallow_drop.h canonicalizer/enum_drop_redundant_validation.h canonicalizer/enum_filter_by_type.h canonicalizer/exclusive_maximum_boolean_integer_fold.h @@ -43,6 +48,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema canonicalizer/optional_property_implicit.h canonicalizer/recursive_anchor_false_drop.h canonicalizer/required_property_implicit.h + canonicalizer/required_to_extends.h canonicalizer/single_branch_allof.h canonicalizer/single_branch_anyof.h canonicalizer/single_branch_oneof.h @@ -50,6 +56,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema canonicalizer/type_boolean_as_enum.h canonicalizer/type_inherit_in_place.h canonicalizer/type_null_as_enum.h + canonicalizer/type_union_distribute_keywords.h canonicalizer/type_union_implicit.h canonicalizer/type_union_to_schemas.h canonicalizer/type_with_applicator_to_allof.h diff --git a/vendor/blaze/src/alterschema/alterschema.cc b/vendor/blaze/src/alterschema/alterschema.cc index 4e511019..efadc22f 100644 --- a/vendor/blaze/src/alterschema/alterschema.cc +++ b/vendor/blaze/src/alterschema/alterschema.cc @@ -118,14 +118,19 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "canonicalizer/dependent_required_to_any_of.h" #include "canonicalizer/dependent_schemas_to_any_of.h" #include "canonicalizer/deprecated_false_drop.h" +#include "canonicalizer/disallow_array_to_extends.h" +#include "canonicalizer/disallow_extends_to_type.h" #include "canonicalizer/disallow_to_array_of_schemas.h" +#include "canonicalizer/disallow_type_union_to_extends.h" #include "canonicalizer/divisible_by_implicit.h" #include "canonicalizer/draft3_type_any.h" +#include "canonicalizer/duplicate_disallow_entries.h" #include "canonicalizer/empty_definitions_drop.h" #include "canonicalizer/empty_defs_drop.h" #include "canonicalizer/empty_dependencies_drop.h" #include "canonicalizer/empty_dependent_required_drop.h" #include "canonicalizer/empty_dependent_schemas_drop.h" +#include "canonicalizer/empty_disallow_drop.h" #include "canonicalizer/enum_drop_redundant_validation.h" #include "canonicalizer/enum_filter_by_type.h" #include "canonicalizer/exclusive_maximum_boolean_integer_fold.h" @@ -151,6 +156,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "canonicalizer/optional_property_implicit.h" #include "canonicalizer/recursive_anchor_false_drop.h" #include "canonicalizer/required_property_implicit.h" +#include "canonicalizer/required_to_extends.h" #include "canonicalizer/single_branch_allof.h" #include "canonicalizer/single_branch_anyof.h" #include "canonicalizer/single_branch_oneof.h" @@ -158,6 +164,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "canonicalizer/type_boolean_as_enum.h" #include "canonicalizer/type_inherit_in_place.h" #include "canonicalizer/type_null_as_enum.h" +#include "canonicalizer/type_union_distribute_keywords.h" #include "canonicalizer/type_union_implicit.h" #include "canonicalizer/type_union_to_schemas.h" #include "canonicalizer/type_with_applicator_to_allof.h" @@ -511,6 +518,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); @@ -523,9 +531,15 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); + bundle.add(); + bundle.add(); + bundle.add(); + bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_array_to_extends.h b/vendor/blaze/src/alterschema/canonicalizer/disallow_array_to_extends.h new file mode 100644 index 00000000..293b9bd5 --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/disallow_array_to_extends.h @@ -0,0 +1,85 @@ +class DisallowArrayToExtends final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DisallowArrayToExtends() + : SchemaTransformRule{ + "disallow_array_to_extends", + "A multi-way `disallow` is the conjunction of single negations: " + "each element becomes its own single-element `disallow` in an " + "`extends` branch"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->size() > 1); + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto branches{JSON::make_array()}; + for (const auto &element : schema.at("disallow").as_array()) { + auto negation{JSON::make_array()}; + negation.push_back(element); + auto branch{JSON::make_object()}; + branch.assign("disallow", std::move(negation)); + branches.push_back(std::move(branch)); + } + + schema.erase("disallow"); + + if (schema.defines("extends") && schema.at("extends").is_array()) { + this->extends_start_ = schema.at("extends").size(); + for (auto &branch : branches.as_array()) { + schema.at("extends").push_back(std::move(branch)); + } + } else if (schema.defines("extends")) { + auto extends{JSON::make_array()}; + extends.push_back(schema.at("extends")); + this->extends_start_ = extends.size(); + for (auto &branch : branches.as_array()) { + extends.push_back(std::move(branch)); + } + schema.assign("extends", std::move(extends)); + } else { + this->extends_start_ = 0; + schema.assign("extends", std::move(branches)); + } + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + const auto disallow_prefix{current.concat({"disallow"})}; + if (!target.starts_with(disallow_prefix)) { + return target; + } + + const auto relative{target.resolve_from(disallow_prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + + const auto index{relative.at(0).to_index()}; + return target.rebase( + current.concat({"disallow", index}), + current.concat( + {"extends", this->extends_start_ + index, "disallow", 0})); + } + +private: + mutable std::size_t extends_start_{0}; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h b/vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h new file mode 100644 index 00000000..7cb88c1b --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h @@ -0,0 +1,109 @@ +class DisallowExtendsToType final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DisallowExtendsToType() + : SchemaTransformRule{ + "disallow_extends_to_type", + "Negating a conjunction is the disjunction of the negations: an " + "`extends` under `disallow` becomes a `type` union where each " + "branch is its own single negation"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->size() == 1); + + const auto &element{disallow->at(0)}; + ONLY_CONTINUE_IF(element.is_object() && element.defines("extends") && + element.at("extends").is_array() && + !element.at("extends").empty()); + + // Only a pure negation can be distributed: the schema must assert nothing + // besides `disallow` (otherwise the new `type` would clobber a sibling + // constraint), and the negated schema must assert nothing besides `extends` + // (otherwise those conjuncts would be silently dropped) + ONLY_CONTINUE_IF( + wraps_single_constraint(schema, "disallow", walker, vocabularies) && + wraps_single_constraint(element, "extends", walker, vocabularies)); + + // The conjuncts relocate to distinct `type` branches (handled by + // `rereference`), but the wrapper schema itself is dissolved rather than + // moved, so a reference straight at it has no new home: bail in that case + static const JSON::String DISALLOW{"disallow"}; + auto wrapper_pointer{location.pointer}; + wrapper_pointer.push_back(std::cref(DISALLOW)); + wrapper_pointer.push_back(static_cast(0)); + ONLY_CONTINUE_IF(!frame.has_references_to(wrapper_pointer)); + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto branches{JSON::make_array()}; + for (auto &branch : schema.at("disallow").at(0).at("extends").as_array()) { + auto negation{JSON::make_array()}; + negation.push_back(std::move(branch)); + auto element{JSON::make_object()}; + element.assign("disallow", std::move(negation)); + branches.push_back(std::move(element)); + } + + schema.erase("disallow"); + schema.assign("type", std::move(branches)); + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + const auto extends_prefix{current.concat({"disallow", 0, "extends"})}; + if (!target.starts_with(extends_prefix)) { + return target; + } + + const auto relative{target.resolve_from(extends_prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + + const auto index{relative.at(0).to_index()}; + return target.rebase(extends_prefix.concat({index}), + current.concat({"type", index, "disallow", 0})); + } + +private: + static auto wraps_single_constraint( + const sourcemeta::core::JSON &schema, const std::string_view keyword, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::Vocabularies &vocabularies) -> bool { + for (const auto &entry : schema.as_object()) { + if (entry.first == keyword) { + continue; + } + + const auto type{walker(entry.first, vocabularies).type}; + if (type != SchemaKeywordType::Annotation && + type != SchemaKeywordType::Comment && + type != SchemaKeywordType::Other && + type != SchemaKeywordType::Unknown && + type != SchemaKeywordType::LocationMembers) { + return false; + } + } + + return true; + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h b/vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h new file mode 100644 index 00000000..6763e68d --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h @@ -0,0 +1,109 @@ +class DisallowTypeUnionToExtends final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DisallowTypeUnionToExtends() + : SchemaTransformRule{ + "disallow_type_union_to_extends", + "Negating a disjunction is the conjunction of the negations: a " + "`type` union under `disallow` becomes an `extends` where each " + "branch is its own single negation"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->size() == 1); + + const auto &element{disallow->at(0)}; + ONLY_CONTINUE_IF(element.is_object() && element.defines("type") && + element.at("type").is_array() && + !element.at("type").empty()); + + // Only a pure negation can be distributed: the schema must assert nothing + // besides `disallow` (otherwise the new `extends` would clobber a sibling + // constraint), and the negated schema must assert nothing besides its + // `type` union (otherwise those conjuncts would be silently dropped) + ONLY_CONTINUE_IF( + wraps_single_constraint(schema, "disallow", walker, vocabularies) && + wraps_single_constraint(element, "type", walker, vocabularies)); + + // The union members relocate to distinct `extends` branches (handled by + // `rereference`), but the wrapper schema itself is dissolved rather than + // moved, so a reference straight at it has no new home: bail in that case + static const JSON::String DISALLOW{"disallow"}; + auto wrapper_pointer{location.pointer}; + wrapper_pointer.push_back(std::cref(DISALLOW)); + wrapper_pointer.push_back(static_cast(0)); + ONLY_CONTINUE_IF(!frame.has_references_to(wrapper_pointer)); + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto branches{JSON::make_array()}; + for (auto &member : schema.at("disallow").at(0).at("type").as_array()) { + auto negation{JSON::make_array()}; + negation.push_back(std::move(member)); + auto branch{JSON::make_object()}; + branch.assign("disallow", std::move(negation)); + branches.push_back(std::move(branch)); + } + + schema.erase("disallow"); + schema.assign("extends", std::move(branches)); + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + const auto type_prefix{current.concat({"disallow", 0, "type"})}; + if (!target.starts_with(type_prefix)) { + return target; + } + + const auto relative{target.resolve_from(type_prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + + const auto index{relative.at(0).to_index()}; + return target.rebase(type_prefix.concat({index}), + current.concat({"extends", index, "disallow", 0})); + } + +private: + static auto wraps_single_constraint( + const sourcemeta::core::JSON &schema, const std::string_view keyword, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::Vocabularies &vocabularies) -> bool { + for (const auto &entry : schema.as_object()) { + if (entry.first == keyword) { + continue; + } + + const auto type{walker(entry.first, vocabularies).type}; + if (type != SchemaKeywordType::Annotation && + type != SchemaKeywordType::Comment && + type != SchemaKeywordType::Other && + type != SchemaKeywordType::Unknown && + type != SchemaKeywordType::LocationMembers) { + return false; + } + } + + return true; + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h b/vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h new file mode 100644 index 00000000..aecdcda2 --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h @@ -0,0 +1,58 @@ +class DuplicateDisallowEntries final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DuplicateDisallowEntries() + : SchemaTransformRule{ + "duplicate_disallow_entries", + "Setting duplicate subschemas in `disallow` is redundant, as " + "negating the same subschema more than once is guaranteed to not " + "affect the validation result"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && !disallow->unique()); + + // Compacting the array would shift the index of every entry that follows a + // removed duplicate, so a reference into `disallow` could silently end up + // pointing at a different subschema. Leave such cases untouched and let + // `DisallowArrayToExtends` split them instead, which preserves every index + // as its own `extends` branch + const std::string keyword{"disallow"}; + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(keyword)})); + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + const auto &original{schema.at("disallow")}; + + std::unordered_set, + HashJSON>, + EqualJSON>> + seen; + auto result{JSON::make_array()}; + + for (const auto &element : original.as_array()) { + if (seen.emplace(std::cref(element)).second) { + result.push_back(element); + } + } + + schema.assign("disallow", std::move(result)); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h b/vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h new file mode 100644 index 00000000..429e85c7 --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h @@ -0,0 +1,29 @@ +class EmptyDisallowDrop final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + EmptyDisallowDrop() : SchemaTransformRule{"empty_disallow_drop", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->empty()); + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.erase("disallow"); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h b/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h index 8a4d9b51..b9233504 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h +++ b/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h @@ -63,6 +63,27 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { continue; } + // In Draft 3 and older, `required` and `optional` are property-presence + // flags read by the parent object validator, not value assertions that an + // `enum` could make redundant + if (entry.first == "required" && + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})) { + continue; + } + + if (entry.first == "optional" && + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_0, + Vocabularies::Known::JSON_Schema_Draft_0_Hyper, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_1_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_2_Hyper})) { + continue; + } + if (entry.second.is_boolean() && entry.second.to_boolean()) { if (!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(entry.first)})) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h b/vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h new file mode 100644 index 00000000..44e9845e --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h @@ -0,0 +1,99 @@ +class RequiredToExtends final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + RequiredToExtends() + : SchemaTransformRule{ + "required_to_extends", + "In Draft 3 canonical form, `required` is only ever a sibling of " + "`extends`; its other siblings are wrapped into an `extends` " + "branch"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_boolean()); + + for (const auto &entry : schema.as_object()) { + if (!stays_at_top(entry.first)) { + return true; + } + } + + return false; + } + + auto transform(JSON &schema, const Result &) const -> void override { + this->wrapped_keywords_.clear(); + for (const auto &entry : schema.as_object()) { + if (!stays_at_top(entry.first)) { + this->wrapped_keywords_.push_back(entry.first); + } + } + + auto branch{JSON::make_object()}; + for (const auto &keyword : this->wrapped_keywords_) { + branch.assign(keyword, schema.at(keyword)); + } + + for (const auto &keyword : this->wrapped_keywords_) { + schema.erase(keyword); + } + + if (schema.defines("extends") && schema.at("extends").is_array()) { + this->branch_index_ = schema.at("extends").size(); + schema.at("extends").push_back(std::move(branch)); + } else if (schema.defines("extends")) { + // Draft 3 allows `extends` to be a single schema; preserve it as the + // first branch of the new array + auto extends{JSON::make_array()}; + extends.push_back(schema.at("extends")); + this->branch_index_ = extends.size(); + extends.push_back(std::move(branch)); + schema.assign("extends", std::move(extends)); + } else { + this->branch_index_ = 0; + auto extends{JSON::make_array()}; + extends.push_back(std::move(branch)); + schema.assign("extends", std::move(extends)); + } + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + for (const auto &keyword : this->wrapped_keywords_) { + const auto keyword_prefix{current.concat({keyword})}; + if (target.starts_with(keyword_prefix)) { + return target.rebase( + keyword_prefix, + current.concat({"extends", this->branch_index_, keyword})); + } + } + + return target; + } + +private: + static auto stays_at_top(const sourcemeta::core::JSON::String &keyword) + -> bool { + return keyword == "required" || keyword == "extends" || + keyword == "$schema" || keyword == "id" || keyword == "$ref"; + } + + mutable std::vector wrapped_keywords_; + mutable std::size_t branch_index_{0}; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h b/vendor/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h new file mode 100644 index 00000000..04ba7d5e --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h @@ -0,0 +1,207 @@ +class TypeUnionDistributeKeywords final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + TypeUnionDistributeKeywords() + : SchemaTransformRule{ + "type_union_distribute_keywords", + "A type-specific keyword sibling to a `type` union belongs inside " + "the branch of the type that it applies to"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_array() && !type->empty()); + for (const auto &branch : type->as_array()) { + ONLY_CONTINUE_IF(branch.is_object()); + } + + this->moves_.clear(); + this->wrap_keywords_.clear(); + this->wrap_ = false; + std::vector movable; + for (const auto &entry : schema.as_object()) { + // `required` is a property-presence flag, not a value assertion, so it + // is never pushed into a branch + if (entry.first == "type" || entry.first == "required") { + continue; + } + + const auto &metadata{walker(entry.first, vocabularies)}; + if (metadata.type == sourcemeta::blaze::SchemaKeywordType::Reference) { + continue; + } + + // A keyword that applies to every type carries no type-specific + // information to push down into a branch + if (metadata.instances.none()) { + continue; + } + + movable.push_back(entry.first); + + std::vector targets; + bool has_match{false}; + bool conflict{false}; + for (std::size_t index = 0; index < type->size(); ++index) { + const auto branch_types{branch_type_set(type->at(index))}; + if ((branch_types & metadata.instances).none()) { + continue; + } + + has_match = true; + // A matching branch already constrains this keyword, so distributing + // and erasing the sibling could drop the top-level bound from that + // branch. Wrap instead so nothing is weakened. + if (type->at(index).defines(entry.first)) { + conflict = true; + break; + } + + targets.push_back(index); + } + + if (!has_match || conflict) { + this->wrap_ = true; + } else { + this->moves_.emplace_back(entry.first, std::move(targets)); + } + } + + ONLY_CONTINUE_IF(!movable.empty()); + if (this->wrap_) { + this->moves_.clear(); + this->wrap_keywords_ = std::move(movable); + } + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + if (this->wrap_) { + auto union_branch{JSON::make_object()}; + union_branch.assign("type", schema.at("type")); + auto sibling_branch{JSON::make_object()}; + for (const auto &keyword : this->wrap_keywords_) { + sibling_branch.assign(keyword, schema.at(keyword)); + } + + schema.erase("type"); + for (const auto &keyword : this->wrap_keywords_) { + schema.erase(keyword); + } + + if (schema.defines("extends") && schema.at("extends").is_array()) { + this->type_index_ = schema.at("extends").size(); + schema.at("extends").push_back(std::move(union_branch)); + this->sibling_index_ = schema.at("extends").size(); + schema.at("extends").push_back(std::move(sibling_branch)); + } else { + auto extends{JSON::make_array()}; + this->type_index_ = 0; + extends.push_back(std::move(union_branch)); + this->sibling_index_ = 1; + extends.push_back(std::move(sibling_branch)); + schema.assign("extends", std::move(extends)); + } + + return; + } + + for (const auto &entry : this->moves_) { + const auto value{schema.at(entry.first)}; + auto &type{schema.at("type")}; + for (const auto index : entry.second) { + type.at(index).assign(entry.first, value); + } + } + + for (const auto &entry : this->moves_) { + schema.erase(entry.first); + } + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + if (this->wrap_) { + const auto type_prefix{current.concat({"type"})}; + if (target.starts_with(type_prefix)) { + return target.rebase( + type_prefix, + current.concat({"extends", this->type_index_, "type"})); + } + + for (const auto &keyword : this->wrap_keywords_) { + const auto keyword_prefix{current.concat({keyword})}; + if (target.starts_with(keyword_prefix)) { + return target.rebase( + keyword_prefix, + current.concat({"extends", this->sibling_index_, keyword})); + } + } + + return target; + } + + for (const auto &entry : this->moves_) { + if (entry.second.empty()) { + continue; + } + + const auto keyword_prefix{current.concat({entry.first})}; + if (target.starts_with(keyword_prefix)) { + return target.rebase( + keyword_prefix, + current.concat({"type", entry.second.front(), entry.first})); + } + } + + return target; + } + +private: + static auto branch_type_set(const sourcemeta::core::JSON &branch) + -> sourcemeta::core::JSON::TypeSet { + if (!branch.is_object()) { + return {}; + } + + const auto *type{branch.try_at("type")}; + if (type && (type->is_string() || type->is_array())) { + return parse_schema_type(*type); + } + + const auto *enum_value{branch.try_at("enum")}; + if (enum_value && enum_value->is_array()) { + sourcemeta::core::JSON::TypeSet result; + for (const auto &value : enum_value->as_array()) { + result.set(std::to_underlying(value.type())); + } + return result; + } + + return {}; + } + + mutable std::vector< + std::pair>> + moves_; + mutable std::vector wrap_keywords_; + mutable bool wrap_{false}; + mutable std::size_t type_index_{0}; + mutable std::size_t sibling_index_{0}; +}; diff --git a/vendor/blaze/src/compiler/compile_helpers.h b/vendor/blaze/src/compiler/compile_helpers.h index 8bfb3341..a36190e9 100644 --- a/vendor/blaze/src/compiler/compile_helpers.h +++ b/vendor/blaze/src/compiler/compile_helpers.h @@ -409,7 +409,10 @@ inline auto required_properties(const SchemaContext &schema_context) schema_context.schema.at("properties").is_object()) { for (const auto &entry : schema_context.schema.at("properties").as_object()) { + // In Draft 3, keywords sibling to `$ref` are never evaluated, so a + // `required` flag next to a `$ref` does not make the property mandatory if (entry.second.is_object() && entry.second.defines("required") && + !entry.second.defines("$ref") && entry.second.at("required").is_boolean() && entry.second.at("required").to_boolean()) { result.insert(entry.first); diff --git a/vendor/blaze/src/frame/frame.cc b/vendor/blaze/src/frame/frame.cc index e5d24fbc..4fca9ba5 100644 --- a/vendor/blaze/src/frame/frame.cc +++ b/vendor/blaze/src/frame/frame.cc @@ -184,11 +184,17 @@ auto find_anchors(const sourcemeta::core::JSON &schema, } } - // Draft 4 + // Draft 4 and 3 // Old `id` anchor form if (schema.is_object() && - vocabularies.contains( - sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_4)) { + (vocabularies.contains( + sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_4) || + vocabularies.contains( + sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_4_Hyper) || + vocabularies.contains( + sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_3) || + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_Draft_3_Hyper))) { const auto *id_value{schema.try_at("id")}; if (id_value) { assert(id_value->is_string()); @@ -196,7 +202,7 @@ auto find_anchors(const sourcemeta::core::JSON &schema, // A bare "#" carries no anchor name, so we treat it as no anchor at // all. if (id_view.starts_with('#') && id_view.size() > 1) { - // Draft 4 imposes no plain-name pattern on the fragment, but the + // Draft 4 and 3 impose no plain-name pattern on the fragment, but the // value must still be a valid URI reference per RFC 3986 if (!sourcemeta::core::URI::is_uri_reference(id_view)) { throw sourcemeta::blaze::SchemaKeywordError( @@ -315,6 +321,8 @@ auto supports_id_anchors( case SchemaBaseDialect::JSON_Schema_Draft_6_Hyper: case SchemaBaseDialect::JSON_Schema_Draft_4: case SchemaBaseDialect::JSON_Schema_Draft_4_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_3: + case SchemaBaseDialect::JSON_Schema_Draft_3_Hyper: return true; default: return false; diff --git a/vendor/core/CMakeLists.txt b/vendor/core/CMakeLists.txt index 512e0d3e..158c5bcf 100644 --- a/vendor/core/CMakeLists.txt +++ b/vendor/core/CMakeLists.txt @@ -31,6 +31,7 @@ option(SOURCEMETA_CORE_YAML "Build the Sourcemeta Core YAML library" ON) option(SOURCEMETA_CORE_JSONRPC "Build the Sourcemeta Core JSON-RPC library" ON) option(SOURCEMETA_CORE_MCP "Build the Sourcemeta Core MCP library" ON) option(SOURCEMETA_CORE_HTTP "Build the Sourcemeta Core HTTP library" ON) +option(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL "Use system cURL for the Sourcemeta Core HTTP library" OFF) option(SOURCEMETA_CORE_JOSE "Build the Sourcemeta Core JOSE library" ON) option(SOURCEMETA_CORE_SEMVER "Build the Sourcemeta Core SemVer library" ON) option(SOURCEMETA_CORE_GZIP "Build the Sourcemeta Core GZIP library" ON) @@ -38,6 +39,7 @@ option(SOURCEMETA_CORE_HTML "Build the Sourcemeta Core HTML library" ON) option(SOURCEMETA_CORE_CSS "Build the Sourcemeta Core CSS library" ON) option(SOURCEMETA_CORE_MARKDOWN "Build the Sourcemeta Core Markdown library" ON) option(SOURCEMETA_CORE_TESTS "Build the Sourcemeta Core tests" OFF) +option(SOURCEMETA_CORE_TESTS_CI "Build the Sourcemeta Core CI tests" OFF) option(SOURCEMETA_CORE_BENCHMARK "Build the Sourcemeta Core benchmarks" OFF) option(SOURCEMETA_CORE_DOCS "Build the Sourcemeta Core docs" OFF) option(SOURCEMETA_CORE_INSTALL "Install the Sourcemeta Core library" ON) @@ -230,7 +232,7 @@ endif() # Testing -if(SOURCEMETA_CORE_CONTRIB_GOOGLETEST OR SOURCEMETA_CORE_TESTS) +if(SOURCEMETA_CORE_CONTRIB_GOOGLETEST OR SOURCEMETA_CORE_TESTS OR SOURCEMETA_CORE_TESTS_CI) find_package(GoogleTest REQUIRED) endif() @@ -380,6 +382,14 @@ if(SOURCEMETA_CORE_TESTS) endif() endif() +if(SOURCEMETA_CORE_TESTS_CI) + enable_testing() + + if(SOURCEMETA_CORE_HTTP) + add_subdirectory(test/http/ci) + endif() +endif() + if(SOURCEMETA_CORE_BENCHMARK) add_subdirectory(benchmark) endif() diff --git a/vendor/core/DEPENDENCIES b/vendor/core/DEPENDENCIES index 73fe46d9..fb683d43 100644 --- a/vendor/core/DEPENDENCIES +++ b/vendor/core/DEPENDENCIES @@ -3,9 +3,11 @@ jsontestsuite https://github.com/nst/JSONTestSuite d64aefb55228d9584d3e5b2433f72 yaml-test-suite https://github.com/yaml/yaml-test-suite data-2022-01-17 cmark-gfm https://github.com/github/cmark-gfm 587a12bb54d95ac37241377e6ddc93ea0e45439b uritemplate-test https://github.com/uri-templates/uritemplate-test 1eb27ab4462b9e5819dc47db99044f5fd1fa9bc7 -pyca-cryptography https://github.com/pyca/cryptography c4935a7021af37c38e0684b0546c1b4378518342 +pyca-cryptography https://github.com/pyca/cryptography 9747d06e83764e7f1ea4c04daf134cb8f861700b +wycheproof https://github.com/C2SP/wycheproof 6d7cccd0fcb1917368579adeeac10fe802f1b521 pcre2 https://github.com/PCRE2Project/pcre2 pcre2-10.47 googletest https://github.com/google/googletest a7f443b80b105f940225332ed3c31f2790092f47 googlebenchmark https://github.com/google/benchmark 378fe693a1ef51500db21b11ff05a8018c5f0e55 libdeflate https://github.com/ebiggers/libdeflate v1.25 unicodetools https://github.com/unicode-org/unicodetools final-17.0-20250910 +jose-cookbook https://github.com/ietf-jose/cookbook 13692b68bfc18b99557a5b1ed311fd5077bfff04 diff --git a/vendor/core/cmake/common/compiler/options.cmake b/vendor/core/cmake/common/compiler/options.cmake index 15079925..20c05eaa 100644 --- a/vendor/core/cmake/common/compiler/options.cmake +++ b/vendor/core/cmake/common/compiler/options.cmake @@ -116,6 +116,10 @@ function(sourcemeta_add_default_options visibility target) -Wno-exit-time-destructors -Wrange-loop-analysis + # Manage Objective-C and Objective-C++ object lifetimes with Automatic + # Reference Counting + $<$,$>:-fobjc-arc> + # Enable loop vectorization for performance reasons $<$>:-fvectorize> # Enable vectorization of straight-line code for performance diff --git a/vendor/core/cmake/common/variables.cmake b/vendor/core/cmake/common/variables.cmake index ee6359cb..c7d6adbb 100644 --- a/vendor/core/cmake/common/variables.cmake +++ b/vendor/core/cmake/common/variables.cmake @@ -1,3 +1,10 @@ +# Objective-C++ powers the Apple-specific backends and must be enabled before +# we capture the project languages below, so its standard and visibility +# defaults get applied +if(APPLE) + enable_language(OBJCXX) +endif() + # Get the list of languages defined in the project get_property(SOURCEMETA_LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES) diff --git a/vendor/core/config.cmake.in b/vendor/core/config.cmake.in index d4511d6c..16d87fed 100644 --- a/vendor/core/config.cmake.in +++ b/vendor/core/config.cmake.in @@ -158,6 +158,9 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonrpc.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_mcp.cmake") elseif(component STREQUAL "http") + if(@SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL@) + find_dependency(CURL) + endif() include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") @@ -175,6 +178,7 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_time.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_crypto.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jose.cmake") diff --git a/vendor/core/src/core/crypto/CMakeLists.txt b/vendor/core/src/core/crypto/CMakeLists.txt index 17c5104a..679bfd55 100644 --- a/vendor/core/src/core/crypto/CMakeLists.txt +++ b/vendor/core/src/core/crypto/CMakeLists.txt @@ -12,30 +12,127 @@ if(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) target_sources(sourcemeta_core_crypto PRIVATE crypto_sha1_openssl.cc crypto_sha256_openssl.cc crypto_sha384_openssl.cc crypto_sha512_openssl.cc crypto_random_openssl.cc - crypto_verify_rsa_openssl.cc crypto_verify_ecdsa_openssl.cc) + crypto_verify_openssl.cc) target_link_libraries(sourcemeta_core_crypto PRIVATE OpenSSL::Crypto) elseif(APPLE) + enable_language(OBJCXX) + + # Ed25519 is verified through CryptoKit (Swift), reached from Objective-C++. + # The Swift shim is compiled to an object and its generated Objective-C + # interface header out of band, since the rest of the library is C++ + set(CRYPTOKIT_SWIFT "${CMAKE_CURRENT_SOURCE_DIR}/crypto_eddsa_cryptokit.swift") + set(CRYPTOKIT_HEADER + "${CMAKE_CURRENT_BINARY_DIR}/sourcemeta_core_cryptokit-Swift.h") + if(CMAKE_OSX_SYSROOT) + set(CRYPTOKIT_SDK "${CMAKE_OSX_SYSROOT}") + else() + execute_process(COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE CRYPTOKIT_SDK OUTPUT_STRIP_TRAILING_WHITESPACE) + endif() + + # The toolchain directory holding the Swift runtime back-deployment archives + # that the shim autolinks for older deployment targets + execute_process(COMMAND xcrun --find swiftc + OUTPUT_VARIABLE CRYPTOKIT_SWIFTC OUTPUT_STRIP_TRAILING_WHITESPACE) + get_filename_component(CRYPTOKIT_TOOLCHAIN_BIN "${CRYPTOKIT_SWIFTC}" DIRECTORY) + get_filename_component(CRYPTOKIT_RUNTIME_LIB + "${CRYPTOKIT_TOOLCHAIN_BIN}/../lib/swift/macosx" ABSOLUTE) + if(CMAKE_OSX_DEPLOYMENT_TARGET) + set(CRYPTOKIT_DEPLOYMENT "${CMAKE_OSX_DEPLOYMENT_TARGET}") + else() + # CryptoKit signing over Curve25519 is available since macOS 10.15 + set(CRYPTOKIT_DEPLOYMENT "10.15") + endif() + if(CMAKE_OSX_ARCHITECTURES) + set(CRYPTOKIT_ARCHITECTURES ${CMAKE_OSX_ARCHITECTURES}) + else() + set(CRYPTOKIT_ARCHITECTURES "${CMAKE_SYSTEM_PROCESSOR}") + endif() + + # One object per architecture, emitting the architecture independent header + # only on the first, combined afterwards into a single object + set(CRYPTOKIT_OBJECTS) + set(CRYPTOKIT_HEADER_OUTPUT "${CRYPTOKIT_HEADER}") + set(CRYPTOKIT_EMIT_HEADER + -emit-objc-header -emit-objc-header-path "${CRYPTOKIT_HEADER}") + foreach(architecture IN LISTS CRYPTOKIT_ARCHITECTURES) + set(CRYPTOKIT_ARCH_OBJECT + "${CMAKE_CURRENT_BINARY_DIR}/crypto_eddsa_cryptokit_${architecture}.o") + add_custom_command( + OUTPUT "${CRYPTOKIT_ARCH_OBJECT}" ${CRYPTOKIT_HEADER_OUTPUT} + COMMAND xcrun swiftc + -sdk "${CRYPTOKIT_SDK}" + -target "${architecture}-apple-macosx${CRYPTOKIT_DEPLOYMENT}" + -module-name sourcemeta_core_cryptokit + -parse-as-library -O + ${CRYPTOKIT_EMIT_HEADER} + -emit-object -o "${CRYPTOKIT_ARCH_OBJECT}" + "${CRYPTOKIT_SWIFT}" + DEPENDS "${CRYPTOKIT_SWIFT}" + COMMENT "Building CryptoKit Swift shim (${architecture})" + VERBATIM) + list(APPEND CRYPTOKIT_OBJECTS "${CRYPTOKIT_ARCH_OBJECT}") + set(CRYPTOKIT_HEADER_OUTPUT) + set(CRYPTOKIT_EMIT_HEADER) + endforeach() + + list(LENGTH CRYPTOKIT_OBJECTS CRYPTOKIT_OBJECT_COUNT) + if(CRYPTOKIT_OBJECT_COUNT GREATER 1) + set(CRYPTOKIT_OBJECT "${CMAKE_CURRENT_BINARY_DIR}/crypto_eddsa_cryptokit.o") + add_custom_command( + OUTPUT "${CRYPTOKIT_OBJECT}" + COMMAND lipo -create ${CRYPTOKIT_OBJECTS} -output "${CRYPTOKIT_OBJECT}" + DEPENDS ${CRYPTOKIT_OBJECTS} + COMMENT "Combining CryptoKit Swift shim architectures" + VERBATIM) + else() + set(CRYPTOKIT_OBJECT "${CRYPTOKIT_OBJECTS}") + endif() + + set_source_files_properties("${CRYPTOKIT_OBJECT}" + PROPERTIES EXTERNAL_OBJECT TRUE GENERATED TRUE) + set_source_files_properties("${CRYPTOKIT_HEADER}" PROPERTIES GENERATED TRUE) + set_source_files_properties(crypto_eddsa_cryptokit.mm + PROPERTIES OBJECT_DEPENDS "${CRYPTOKIT_HEADER}") + target_sources(sourcemeta_core_crypto PRIVATE crypto_sha1_apple.cc crypto_sha256_apple.cc crypto_sha384_apple.cc crypto_sha512_apple.cc crypto_random_apple.cc - crypto_verify_rsa_apple.cc crypto_verify_ecdsa_apple.cc) + crypto_verify_apple.cc + crypto_eddsa_cryptokit.mm "${CRYPTOKIT_OBJECT}" + crypto_eddsa.h crypto_eddsa_apple.h crypto_bignum.h crypto_shake256.h) + + # The generated Objective-C interface header lives in the build tree + target_include_directories(sourcemeta_core_crypto + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") + target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework Security") target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework CoreFoundation") + target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework CryptoKit") + target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework Foundation") + + # Resolve the Swift runtime that the shim autolinks, both at link and at load. + # PUBLIC rather than INTERFACE so that a shared build of this library, which + # has its own link step pulling in the Swift object, also gets the flags + target_link_options(sourcemeta_core_crypto PUBLIC + "SHELL:-L ${CRYPTOKIT_SDK}/usr/lib/swift" + "SHELL:-L ${CRYPTOKIT_RUNTIME_LIB}" + "SHELL:-Xlinker -rpath -Xlinker /usr/lib/swift") elseif(WIN32) target_sources(sourcemeta_core_crypto PRIVATE crypto_sha1_windows.cc crypto_sha256_windows.cc crypto_sha384_windows.cc crypto_sha512_windows.cc crypto_random_windows.cc - crypto_verify_rsa_windows.cc crypto_verify_ecdsa_windows.cc) + crypto_verify_windows.cc crypto_eddsa.h + crypto_bignum.h crypto_shake256.h) target_link_libraries(sourcemeta_core_crypto PRIVATE bcrypt) else() message(WARNING "Building the reference cryptography backend, instead of the " "OpenSSL recommended production one") target_sources(sourcemeta_core_crypto PRIVATE crypto_sha1_other.cc crypto_sha256_other.cc crypto_sha384_other.cc - crypto_sha512_other.cc crypto_random_other.cc - crypto_verify_rsa_other.cc crypto_verify_ecdsa_other.cc - crypto_bignum.h crypto_ecc.h) + crypto_sha512_other.cc crypto_random_other.cc crypto_verify_other.cc + crypto_bignum.h crypto_ecc.h crypto_eddsa.h crypto_shake256.h) endif() if(SOURCEMETA_CORE_INSTALL) diff --git a/vendor/core/src/core/crypto/crypto_bignum.h b/vendor/core/src/core/crypto/crypto_bignum.h index cd025381..5244bc25 100644 --- a/vendor/core/src/core/crypto/crypto_bignum.h +++ b/vendor/core/src/core/crypto/crypto_bignum.h @@ -3,8 +3,8 @@ // Fixed-capacity unsigned big integer arithmetic for the reference // signature verification backend. Capacity fits 4096-bit RSA operands -// and their double-width products. Performance and constant-time -// execution are non-goals, verification consumes only public inputs +// and their double-width products. Constant-time execution is not +// required, since verification consumes only public inputs #include @@ -61,7 +61,7 @@ inline auto bignum_from_u64(const std::uint64_t value) noexcept -> Bignum { return result; } -inline auto bignum_from_hex(const std::string_view hex) noexcept -> Bignum { +inline auto bignum_from_hex(const std::string_view hex) -> Bignum { const auto nibble{[](const char character) noexcept -> std::uint8_t { if (character >= '0' && character <= '9') { return static_cast(character - '0'); @@ -127,7 +127,12 @@ inline auto bignum_bit_length(const Bignum &value) noexcept -> std::size_t { inline auto bignum_get_bit(const Bignum &value, const std::size_t bit) noexcept -> bool { - return ((value.words[bit / 64] >> (bit % 64)) & 1u) != 0; + const auto word{bit / 64}; + if (word >= value.size) { + return false; + } + + return ((value.words[word] >> (bit % 64)) & 1u) != 0; } // Assumes the result fits in the capacity @@ -173,20 +178,118 @@ inline auto bignum_subtract_in_place(Bignum &left, const Bignum &right) noexcept bignum_normalize(left); } +inline auto bignum_shift_right(const Bignum &value, + const std::size_t bits) noexcept -> Bignum; + +// Reduce a value modulo the modulus with Knuth's Algorithm D (TAOCP Volume 2, +// Section 4.3.1), the schoolbook long division that estimates one quotient +// word per step rather than one bit, so the cost is quadratic in the number of +// words rather than the number of bits inline auto bignum_reduce(Bignum &value, const Bignum &modulus) noexcept -> void { - const auto modulus_bits{bignum_bit_length(modulus)}; - while (bignum_compare(value, modulus) >= 0) { - const auto value_bits{bignum_bit_length(value)}; - auto shift{value_bits - modulus_bits}; - auto shifted{bignum_shift_left(modulus, shift)}; - if (bignum_compare(shifted, value) > 0) { - shift -= 1; - shifted = bignum_shift_left(modulus, shift); + if (bignum_compare(value, modulus) < 0) { + return; + } + + const auto divisor_words{modulus.size}; + + // A single-word divisor folds the value down word by word + if (divisor_words == 1) { + const auto divisor{modulus.words[0]}; + BignumDoubleWord remainder{0}; + for (std::size_t index = value.size; index > 0; --index) { + remainder = (remainder << 64u) | value.words[index - 1]; + remainder %= divisor; } - bignum_subtract_in_place(value, shifted); + value = bignum_from_u64(static_cast(remainder)); + return; } + + // Normalize so the divisor's top word has its high bit set, which bounds the + // error of each quotient word estimate to at most two + const auto shift{static_cast( + (64u - (bignum_bit_length(modulus) % 64u)) % 64u)}; + const auto divisor{shift > 0 ? bignum_shift_left(modulus, shift) : modulus}; + auto dividend{shift > 0 ? bignum_shift_left(value, shift) : value}; + const auto dividend_words{dividend.size}; + const auto quotient_words{dividend_words - divisor_words}; + const auto top{divisor.words[divisor_words - 1]}; + const auto next{divisor.words[divisor_words - 2]}; + const BignumDoubleWord base{static_cast(1) << 64u}; + + for (std::size_t step = quotient_words + 1; step > 0; --step) { + const auto offset{step - 1}; + + // Estimate the quotient word from the top two words of the running value + const auto numerator{ + (static_cast(dividend.words[offset + divisor_words]) + << 64u) | + dividend.words[offset + divisor_words - 1]}; + auto estimate{numerator / top}; + auto estimate_remainder{numerator % top}; + while (estimate >= base || + estimate * next > (estimate_remainder << 64u) + + dividend.words[offset + divisor_words - 2]) { + estimate -= 1; + estimate_remainder += top; + if (estimate_remainder >= base) { + break; + } + } + + // Multiply the divisor by the estimate and subtract from the running value + const auto quotient_word{static_cast(estimate)}; + BignumDoubleWord carry{0}; + std::uint64_t borrow{0}; + for (std::size_t index = 0; index < divisor_words; ++index) { + const auto product{static_cast(quotient_word) * + divisor.words[index] + + carry}; + carry = product >> 64u; + const auto subtrahend{static_cast(product)}; + const auto current{dividend.words[offset + index]}; + const auto without_subtrahend{current - subtrahend}; + auto next_borrow{current < subtrahend ? 1u : 0u}; + const auto result_word{without_subtrahend - borrow}; + if (without_subtrahend < borrow) { + next_borrow += 1u; + } + + dividend.words[offset + index] = result_word; + borrow = next_borrow; + } + + const auto current{dividend.words[offset + divisor_words]}; + const auto subtrahend{static_cast(carry)}; + const auto without_subtrahend{current - subtrahend}; + auto next_borrow{current < subtrahend ? 1u : 0u}; + dividend.words[offset + divisor_words] = without_subtrahend - borrow; + if (without_subtrahend < borrow) { + next_borrow += 1u; + } + + // The estimate was at most one too large, so add the divisor back when the + // subtraction borrowed past the top + if (next_borrow != 0) { + BignumDoubleWord add_carry{0}; + for (std::size_t index = 0; index < divisor_words; ++index) { + const auto sum{ + static_cast(dividend.words[offset + index]) + + divisor.words[index] + add_carry}; + dividend.words[offset + index] = static_cast(sum); + add_carry = sum >> 64u; + } + + dividend.words[offset + divisor_words] += + static_cast(add_carry); + } + } + + // The remainder occupies the low words, still scaled by the normalization + dividend.size = divisor_words; + bignum_normalize(dividend); + value = shift > 0 ? bignum_shift_right(dividend, shift) : dividend; } // Assumes both operands fit in half the capacity @@ -324,13 +427,62 @@ inline auto bignum_mod_multiply(const Bignum &left, const Bignum &right, return result; } -// The modulus must be prime, so that Fermat's little theorem gives the -// inverse as the modulus-minus-two power +// Halve a value modulo an odd modulus: an even value shifts down, an odd one +// becomes even by adding the modulus first, so the result stays an integer +inline auto bignum_mod_halve(const Bignum &value, + const Bignum &modulus) noexcept -> Bignum { + if ((value.words[0] & 1u) == 0) { + return bignum_shift_right(value, 1); + } + + return bignum_shift_right(bignum_add(value, modulus), 1); +} + +// Modular inverse by the binary extended Euclidean algorithm, which needs only +// halving, subtraction, and comparison rather than the modular exponentiation +// a Fermat inverse over a prime modulus would spend. The modulus must be odd. +// Returns zero when the value has no inverse, which is when it shares a factor +// with the modulus or reduces to zero inline auto bignum_mod_inverse(const Bignum &value, const Bignum &modulus) noexcept -> Bignum { - auto exponent{modulus}; - bignum_subtract_in_place(exponent, bignum_from_u64(2)); - return bignum_mod_exp(value, exponent, modulus); + const auto one{bignum_from_u64(1)}; + auto first{value}; + bignum_reduce(first, modulus); + auto second{modulus}; + auto first_coefficient{one}; + Bignum second_coefficient; + + while (bignum_compare(first, one) != 0 && bignum_compare(second, one) != 0) { + // A side reaching zero means the greatest common divisor exceeds one, so no + // inverse exists. Stopping here also keeps the halving below from spinning + // forever on a zero value + if (bignum_is_zero(first) || bignum_is_zero(second)) { + return {}; + } + + while ((first.words[0] & 1u) == 0) { + first = bignum_shift_right(first, 1); + first_coefficient = bignum_mod_halve(first_coefficient, modulus); + } + + while ((second.words[0] & 1u) == 0) { + second = bignum_shift_right(second, 1); + second_coefficient = bignum_mod_halve(second_coefficient, modulus); + } + + if (bignum_compare(first, second) >= 0) { + bignum_subtract_in_place(first, second); + first_coefficient = + bignum_mod_subtract(first_coefficient, second_coefficient, modulus); + } else { + bignum_subtract_in_place(second, first); + second_coefficient = + bignum_mod_subtract(second_coefficient, first_coefficient, modulus); + } + } + + return bignum_compare(first, one) == 0 ? first_coefficient + : second_coefficient; } inline auto bignum_to_bytes(const Bignum &value, const std::size_t length) diff --git a/vendor/core/src/core/crypto/crypto_ecc.h b/vendor/core/src/core/crypto/crypto_ecc.h index 04f53d98..2a4de5e0 100644 --- a/vendor/core/src/core/crypto/crypto_ecc.h +++ b/vendor/core/src/core/crypto/crypto_ecc.h @@ -4,16 +4,22 @@ // Short Weierstrass elliptic curve arithmetic over the NIST prime curves // for the reference signature verification backend. Points are kept in // Jacobian coordinates so that scalar multiplication needs a single modular -// inversion at the end rather than one per step. Performance and constant -// time execution are non-goals, verification consumes only public inputs +// inversion at the end rather than one per step. Constant time execution is +// not required, since verification consumes only public inputs #include "crypto_bignum.h" +#include // std::array #include // std::size_t +#include // std::uint8_t, std::uint64_t #include // std::string_view namespace sourcemeta::core { +// Identifies the field prime so that modular reduction can take the fast +// generalized Mersenne path specific to each NIST curve +enum class NISTPrime : std::uint8_t { P256, P384, P521 }; + struct EllipticCurveParameters { Bignum prime; Bignum coefficient_a; @@ -22,6 +28,7 @@ struct EllipticCurveParameters { Bignum generator_y; Bignum order; std::size_t field_bytes; + NISTPrime reduction; }; // A point in Jacobian coordinates, where the affine point is @@ -34,19 +41,26 @@ struct JacobianPoint { // FIPS 186-4 Appendix D.1.2 curve domain parameters inline auto curve_p256() -> EllipticCurveParameters { - return {bignum_from_hex("ffffffff00000001000000000000000000000000ffffffff" - "ffffffffffffffff"), - bignum_from_hex("ffffffff00000001000000000000000000000000ffffffff" - "fffffffffffffffc"), - bignum_from_hex("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f6" - "3bce3c3e27d2604b"), - bignum_from_hex("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0" - "f4a13945d898c296"), - bignum_from_hex("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ece" - "cbb6406837bf51f5"), - bignum_from_hex("ffffffff00000000ffffffffffffffffbce6faada7179e84" - "f3b9cac2fc632551"), - 32}; + return {.prime = + bignum_from_hex("ffffffff00000001000000000000000000000000ffffffff" + "ffffffffffffffff"), + .coefficient_a = + bignum_from_hex("ffffffff00000001000000000000000000000000ffffffff" + "fffffffffffffffc"), + .coefficient_b = + bignum_from_hex("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f6" + "3bce3c3e27d2604b"), + .generator_x = + bignum_from_hex("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0" + "f4a13945d898c296"), + .generator_y = + bignum_from_hex("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ece" + "cbb6406837bf51f5"), + .order = + bignum_from_hex("ffffffff00000000ffffffffffffffffbce6faada7179e84" + "f3b9cac2fc632551"), + .field_bytes = 32, + .reduction = NISTPrime::P256}; } inline auto curve_p384() -> EllipticCurveParameters { @@ -54,29 +68,233 @@ inline auto curve_p384() -> EllipticCurveParameters { // is ever lost across a line break // clang-format off return { - bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff"), - bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc"), - bignum_from_hex("b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef"), - bignum_from_hex("aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7"), - bignum_from_hex("3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f"), - bignum_from_hex("ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973"), - 48}; + .prime = bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff"), + .coefficient_a = bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc"), + .coefficient_b = bignum_from_hex("b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef"), + .generator_x = bignum_from_hex("aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7"), + .generator_y = bignum_from_hex("3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f"), + .order = bignum_from_hex("ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973"), + .field_bytes = 48, + .reduction = NISTPrime::P384}; // clang-format on } inline auto curve_p521() -> EllipticCurveParameters { // clang-format off return { - bignum_from_hex("01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), - bignum_from_hex("01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc"), - bignum_from_hex("0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00"), - bignum_from_hex("00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66"), - bignum_from_hex("011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650"), - bignum_from_hex("01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409"), - 66}; + .prime = bignum_from_hex("01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + .coefficient_a = bignum_from_hex("01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc"), + .coefficient_b = bignum_from_hex("0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00"), + .generator_x = bignum_from_hex("00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66"), + .generator_y = bignum_from_hex("011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650"), + .order = bignum_from_hex("01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409"), + .field_bytes = 66, + .reduction = NISTPrime::P521}; // clang-format on } +// NIST P-521 field reduction. The prime is 2^521 - 1, so 2^521 is congruent +// to 1 modulo it, and a value below the square of the prime folds the bits +// above position 521 back into the low 521 bits with a single addition +inline auto field_reduce_p521(Bignum &value, const Bignum &prime) noexcept + -> void { + auto high{bignum_shift_right(value, 521)}; + if (value.size > 8) { + value.words[8] &= 0x1ffULL; + for (std::size_t index = 9; index < value.size; ++index) { + value.words[index] = 0; + } + + value.size = 9; + bignum_normalize(value); + } + + value = bignum_add(value, high); + while (bignum_compare(value, prime) >= 0) { + bignum_subtract_in_place(value, prime); + } +} + +// Read the 32-bit limb at the given position, counting from the least +// significant, returning zero past the end of the value +inline auto field_word(const Bignum &value, const std::size_t index) noexcept + -> std::uint64_t { + const auto word{index / 2}; + if (word >= value.size) { + return 0; + } + + return (value.words[word] >> (32 * (index % 2))) & 0xffffffffULL; +} + +// Add a reduction term, given as its 32-bit limbs most significant first, +// into the column accumulator scaled by the multiplier. Column zero is the +// least significant, and accumulating into 64-bit columns rather than +// materializing a value per term keeps the reduction free of temporaries +template +inline auto field_accumulate(std::array &columns, + const std::array &limbs, + const std::uint64_t multiplier) noexcept -> void { + for (std::size_t index = 0; index < Count; ++index) { + columns[Count - 1 - index] += multiplier * limbs[index]; + } +} + +// Carry propagate the 32-bit columns and pack them into a value +template +inline auto field_from_columns(std::array &columns) + -> Bignum { + std::uint64_t carry{0}; + Bignum result; + for (std::size_t index = 0; index <= Count; ++index) { + const auto current{columns[index] + carry}; + result.words[index / 2] |= (current & 0xffffffffULL) << (32 * (index % 2)); + carry = current >> 32; + } + + result.size = (Count + 2) / 2; + bignum_normalize(result); + return result; +} + +// Combine the positive and negative column sums of a generalized Mersenne +// reduction into the single reduced value below the prime +inline auto field_combine(Bignum &positive, const Bignum &negative, + const Bignum &prime) noexcept -> void { + while (bignum_compare(positive, negative) < 0) { + positive = bignum_add(positive, prime); + } + + bignum_subtract_in_place(positive, negative); + while (bignum_compare(positive, prime) >= 0) { + bignum_subtract_in_place(positive, prime); + } +} + +// NIST P-256 field reduction. The prime is 2^256 - 2^224 + 2^192 + 2^96 - 1, +// a generalized Mersenne prime whose reduction recombines the 32-bit limbs of +// the product into a small signed sum of nine field-width terms +// (FIPS 186-4 Appendix D.2.3) +inline auto field_reduce_p256(Bignum &value, const Bignum &prime) noexcept + -> void { + std::array c{}; + for (std::size_t index = 0; index < 16; ++index) { + c[index] = field_word(value, index); + } + + std::array positive_columns{}; + std::array negative_columns{}; + field_accumulate<8>(positive_columns, + {{c[7], c[6], c[5], c[4], c[3], c[2], c[1], c[0]}}, 1); + field_accumulate<8>(positive_columns, + {{c[15], c[14], c[13], c[12], c[11], 0, 0, 0}}, 2); + field_accumulate<8>(positive_columns, + {{0, c[15], c[14], c[13], c[12], 0, 0, 0}}, 2); + field_accumulate<8>(positive_columns, + {{c[15], c[14], 0, 0, 0, c[10], c[9], c[8]}}, 1); + field_accumulate<8>(positive_columns, + {{c[8], c[13], c[15], c[14], c[13], c[11], c[10], c[9]}}, + 1); + field_accumulate<8>(negative_columns, + {{c[10], c[8], 0, 0, 0, c[13], c[12], c[11]}}, 1); + field_accumulate<8>(negative_columns, + {{c[11], c[9], 0, 0, c[15], c[14], c[13], c[12]}}, 1); + field_accumulate<8>(negative_columns, + {{c[12], 0, c[10], c[9], c[8], c[15], c[14], c[13]}}, 1); + field_accumulate<8>(negative_columns, + {{c[13], 0, c[11], c[10], c[9], 0, c[15], c[14]}}, 1); + + auto positive{field_from_columns<8>(positive_columns)}; + const auto negative{field_from_columns<8>(negative_columns)}; + field_combine(positive, negative, prime); + value = positive; +} + +// NIST P-384 field reduction. The prime is 2^384 - 2^128 - 2^96 + 2^32 - 1, +// a generalized Mersenne prime whose reduction recombines the 32-bit limbs of +// the product into a small signed sum of ten field-width terms +// (FIPS 186-4 Appendix D.2.4) +inline auto field_reduce_p384(Bignum &value, const Bignum &prime) noexcept + -> void { + std::array c{}; + for (std::size_t index = 0; index < 24; ++index) { + c[index] = field_word(value, index); + } + + std::array positive_columns{}; + std::array negative_columns{}; + field_accumulate<12>(positive_columns, + {{c[11], c[10], c[9], c[8], c[7], c[6], c[5], c[4], c[3], + c[2], c[1], c[0]}}, + 1); + field_accumulate<12>(positive_columns, + {{0, 0, 0, 0, 0, c[23], c[22], c[21], 0, 0, 0, 0}}, 2); + field_accumulate<12>(positive_columns, + {{c[23], c[22], c[21], c[20], c[19], c[18], c[17], c[16], + c[15], c[14], c[13], c[12]}}, + 1); + field_accumulate<12>(positive_columns, + {{c[20], c[19], c[18], c[17], c[16], c[15], c[14], c[13], + c[12], c[23], c[22], c[21]}}, + 1); + field_accumulate<12>(positive_columns, + {{c[19], c[18], c[17], c[16], c[15], c[14], c[13], c[12], + c[20], 0, c[23], 0}}, + 1); + field_accumulate<12>(positive_columns, + {{0, 0, 0, 0, c[23], c[22], c[21], c[20], 0, 0, 0, 0}}, + 1); + field_accumulate<12>(positive_columns, + {{0, 0, 0, 0, 0, 0, c[23], c[22], c[21], 0, 0, c[20]}}, + 1); + field_accumulate<12>(negative_columns, + {{c[22], c[21], c[20], c[19], c[18], c[17], c[16], c[15], + c[14], c[13], c[12], c[23]}}, + 1); + field_accumulate<12>(negative_columns, + {{0, 0, 0, 0, 0, 0, 0, c[23], c[22], c[21], c[20], 0}}, + 1); + field_accumulate<12>(negative_columns, + {{0, 0, 0, 0, 0, 0, 0, c[23], c[23], 0, 0, 0}}, 1); + + auto positive{field_from_columns<12>(positive_columns)}; + const auto negative{field_from_columns<12>(negative_columns)}; + field_combine(positive, negative, prime); + value = positive; +} + +// Reduce a product below the square of the field prime, taking the fast +// generalized Mersenne path for the curve rather than long division +inline auto field_reduce(Bignum &value, + const EllipticCurveParameters &curve) noexcept + -> void { + switch (curve.reduction) { + case NISTPrime::P521: + field_reduce_p521(value, curve.prime); + return; + case NISTPrime::P256: + field_reduce_p256(value, curve.prime); + return; + case NISTPrime::P384: + field_reduce_p384(value, curve.prime); + return; + } +} + +inline auto field_mod_multiply(const Bignum &left, const Bignum &right, + const EllipticCurveParameters &curve) noexcept + -> Bignum { + auto result{bignum_multiply(left, right)}; + field_reduce(result, curve); + return result; +} + +inline auto field_square(const Bignum &value, + const EllipticCurveParameters &curve) noexcept + -> Bignum { + return field_mod_multiply(value, value, curve); +} + inline auto point_is_infinity(const JacobianPoint &point) noexcept -> bool { return bignum_is_zero(point.z); } @@ -87,32 +305,43 @@ inline auto point_double(const JacobianPoint &point, const EllipticCurveParameters &curve) -> JacobianPoint { if (point_is_infinity(point) || bignum_is_zero(point.y)) { - return {Bignum{}, Bignum{}, Bignum{}}; + return {}; } + // The doubling formula for curves with coefficient -3 (EFD dbl-2001-b), + // which trades the coefficient multiplication and a squaring for one more + // subtraction, and computes every small multiple as a chain of modular + // additions so that no division-based reduction is spent on a constant const auto &prime{curve.prime}; - const auto two{bignum_from_u64(2)}; - const auto three{bignum_from_u64(3)}; - const auto y_squared{bignum_mod_multiply(point.y, point.y, prime)}; - auto subterm{bignum_mod_multiply(point.x, y_squared, prime)}; - subterm = bignum_mod_multiply(bignum_from_u64(4), subterm, prime); - const auto x_squared{bignum_mod_multiply(point.x, point.x, prime)}; - const auto z_squared{bignum_mod_multiply(point.z, point.z, prime)}; - const auto z_fourth{bignum_mod_multiply(z_squared, z_squared, prime)}; - const auto slope{bignum_mod_add( - bignum_mod_multiply(three, x_squared, prime), - bignum_mod_multiply(curve.coefficient_a, z_fourth, prime), prime)}; + const auto delta{field_square(point.z, curve)}; + const auto gamma{field_square(point.y, curve)}; + const auto beta{field_mod_multiply(point.x, gamma, curve)}; + const auto difference{ + field_mod_multiply(bignum_mod_subtract(point.x, delta, prime), + bignum_mod_add(point.x, delta, prime), curve)}; + const auto alpha{bignum_mod_add(bignum_mod_add(difference, difference, prime), + difference, prime)}; + const auto two_beta{bignum_mod_add(beta, beta, prime)}; + const auto four_beta{bignum_mod_add(two_beta, two_beta, prime)}; + const auto eight_beta{bignum_mod_add(four_beta, four_beta, prime)}; const auto result_x{ - bignum_mod_subtract(bignum_mod_multiply(slope, slope, prime), - bignum_mod_multiply(two, subterm, prime), prime)}; - const auto y_fourth{bignum_mod_multiply(y_squared, y_squared, prime)}; + bignum_mod_subtract(field_square(alpha, curve), eight_beta, prime)}; + const auto y_plus_z{bignum_mod_add(point.y, point.z, prime)}; + const auto result_z{bignum_mod_subtract( + bignum_mod_subtract(field_square(y_plus_z, curve), gamma, prime), delta, + prime)}; + const auto gamma_squared{field_square(gamma, curve)}; + const auto two_gamma_squared{ + bignum_mod_add(gamma_squared, gamma_squared, prime)}; + const auto four_gamma_squared{ + bignum_mod_add(two_gamma_squared, two_gamma_squared, prime)}; + const auto eight_gamma_squared{ + bignum_mod_add(four_gamma_squared, four_gamma_squared, prime)}; const auto result_y{bignum_mod_subtract( - bignum_mod_multiply(slope, bignum_mod_subtract(subterm, result_x, prime), - prime), - bignum_mod_multiply(bignum_from_u64(8), y_fourth, prime), prime)}; - const auto result_z{bignum_mod_multiply( - bignum_mod_multiply(two, point.y, prime), point.z, prime)}; - return {result_x, result_y, result_z}; + field_mod_multiply(alpha, bignum_mod_subtract(four_beta, result_x, prime), + curve), + eight_gamma_squared, prime)}; + return {.x = result_x, .y = result_y, .z = result_z}; } // Point addition in Jacobian coordinates @@ -127,19 +356,18 @@ inline auto point_add(const JacobianPoint &left, const JacobianPoint &right, } const auto &prime{curve.prime}; - const auto left_z_squared{bignum_mod_multiply(left.z, left.z, prime)}; - const auto right_z_squared{bignum_mod_multiply(right.z, right.z, prime)}; - const auto u1{bignum_mod_multiply(left.x, right_z_squared, prime)}; - const auto u2{bignum_mod_multiply(right.x, left_z_squared, prime)}; - const auto left_z_cubed{bignum_mod_multiply(left_z_squared, left.z, prime)}; - const auto right_z_cubed{ - bignum_mod_multiply(right_z_squared, right.z, prime)}; - const auto s1{bignum_mod_multiply(left.y, right_z_cubed, prime)}; - const auto s2{bignum_mod_multiply(right.y, left_z_cubed, prime)}; + const auto left_z_squared{field_mod_multiply(left.z, left.z, curve)}; + const auto right_z_squared{field_mod_multiply(right.z, right.z, curve)}; + const auto u1{field_mod_multiply(left.x, right_z_squared, curve)}; + const auto u2{field_mod_multiply(right.x, left_z_squared, curve)}; + const auto left_z_cubed{field_mod_multiply(left_z_squared, left.z, curve)}; + const auto right_z_cubed{field_mod_multiply(right_z_squared, right.z, curve)}; + const auto s1{field_mod_multiply(left.y, right_z_cubed, curve)}; + const auto s2{field_mod_multiply(right.y, left_z_cubed, curve)}; if (bignum_compare(u1, u2) == 0) { if (bignum_compare(s1, s2) != 0) { - return {Bignum{}, Bignum{}, Bignum{}}; + return {}; } return point_double(left, curve); @@ -147,31 +375,121 @@ inline auto point_add(const JacobianPoint &left, const JacobianPoint &right, const auto h{bignum_mod_subtract(u2, u1, prime)}; const auto r{bignum_mod_subtract(s2, s1, prime)}; - const auto h_squared{bignum_mod_multiply(h, h, prime)}; - const auto h_cubed{bignum_mod_multiply(h_squared, h, prime)}; - const auto u1_h_squared{bignum_mod_multiply(u1, h_squared, prime)}; + const auto h_squared{field_square(h, curve)}; + const auto h_cubed{field_mod_multiply(h_squared, h, curve)}; + const auto u1_h_squared{field_mod_multiply(u1, h_squared, curve)}; + const auto result_x{bignum_mod_subtract( + bignum_mod_subtract(field_square(r, curve), h_cubed, prime), + field_mod_multiply(bignum_from_u64(2), u1_h_squared, curve), prime)}; + const auto result_y{bignum_mod_subtract( + field_mod_multiply(r, bignum_mod_subtract(u1_h_squared, result_x, prime), + curve), + field_mod_multiply(s1, h_cubed, curve), prime)}; + const auto result_z{ + field_mod_multiply(field_mod_multiply(h, left.z, curve), right.z, curve)}; + return {.x = result_x, .y = result_y, .z = result_z}; +} + +// Add a Jacobian point and an affine point whose Z coordinate is one, the case +// that arises when accumulating the fixed input points in the combined ladder. +// Skipping the second point's Z powers saves several multiplications over the +// general addition (EFD madd-2007-bl) +inline auto point_add_mixed(const JacobianPoint &left, + const JacobianPoint &right, + const EllipticCurveParameters &curve) + -> JacobianPoint { + if (point_is_infinity(left)) { + return right; + } + + if (point_is_infinity(right)) { + return left; + } + + const auto &prime{curve.prime}; + const auto z_squared{field_square(left.z, curve)}; + const auto u2{field_mod_multiply(right.x, z_squared, curve)}; + const auto s2{field_mod_multiply( + right.y, field_mod_multiply(left.z, z_squared, curve), curve)}; + const auto h{bignum_mod_subtract(u2, left.x, prime)}; + + if (bignum_is_zero(h)) { + if (bignum_compare(s2, left.y) == 0) { + return point_double(left, curve); + } + + return {}; + } + + const auto h_squared{field_square(h, curve)}; + const auto two_h_squared{bignum_mod_add(h_squared, h_squared, prime)}; + const auto scaled_h_squared{ + bignum_mod_add(two_h_squared, two_h_squared, prime)}; + const auto j{field_mod_multiply(h, scaled_h_squared, curve)}; + const auto s_difference{bignum_mod_subtract(s2, left.y, prime)}; + const auto r{bignum_mod_add(s_difference, s_difference, prime)}; + const auto v{field_mod_multiply(left.x, scaled_h_squared, curve)}; + const auto two_v{bignum_mod_add(v, v, prime)}; const auto result_x{bignum_mod_subtract( - bignum_mod_subtract(bignum_mod_multiply(r, r, prime), h_cubed, prime), - bignum_mod_multiply(bignum_from_u64(2), u1_h_squared, prime), prime)}; + bignum_mod_subtract(field_mod_multiply(r, r, curve), j, prime), two_v, + prime)}; + const auto y_j{field_mod_multiply(left.y, j, curve)}; + const auto two_y_j{bignum_mod_add(y_j, y_j, prime)}; const auto result_y{bignum_mod_subtract( - bignum_mod_multiply(r, bignum_mod_subtract(u1_h_squared, result_x, prime), - prime), - bignum_mod_multiply(s1, h_cubed, prime), prime)}; - const auto result_z{bignum_mod_multiply(bignum_mod_multiply(h, left.z, prime), - right.z, prime)}; - return {result_x, result_y, result_z}; + field_mod_multiply(r, bignum_mod_subtract(v, result_x, prime), curve), + two_y_j, prime)}; + const auto z_plus_h{bignum_mod_add(left.z, h, prime)}; + const auto result_z{bignum_mod_subtract( + bignum_mod_subtract(field_square(z_plus_h, curve), z_squared, prime), + h_squared, prime)}; + return {.x = result_x, .y = result_y, .z = result_z}; +} + +// Normalize a Jacobian point to the affine representation with Z coordinate +// one, so that later additions can take the cheaper mixed path +inline auto point_to_affine(const JacobianPoint &point, + const EllipticCurveParameters &curve) + -> JacobianPoint { + if (point_is_infinity(point)) { + return {}; + } + + const auto z_inverse{bignum_mod_inverse(point.z, curve.prime)}; + const auto z_inverse_squared{field_square(z_inverse, curve)}; + const auto z_inverse_cubed{ + field_mod_multiply(z_inverse_squared, z_inverse, curve)}; + return {.x = field_mod_multiply(point.x, z_inverse_squared, curve), + .y = field_mod_multiply(point.y, z_inverse_cubed, curve), + .z = bignum_from_u64(1)}; } -inline auto point_scalar_multiply(const Bignum &scalar, - const JacobianPoint &point, - const EllipticCurveParameters &curve) +// Compute scalar_one * point_one + scalar_two * point_two with Shamir's trick, +// a single double-and-add over the longer scalar that adds a precomputed sum +// whenever both scalars have a set bit, halving the doublings of two separate +// scalar multiplications. The three addable points are kept affine so every +// step takes the mixed addition +inline auto point_double_scalar_multiply(const Bignum &scalar_one, + const JacobianPoint &point_one, + const Bignum &scalar_two, + const JacobianPoint &point_two, + const EllipticCurveParameters &curve) -> JacobianPoint { - JacobianPoint result{Bignum{}, Bignum{}, Bignum{}}; - const auto bits{bignum_bit_length(scalar)}; + const auto combined{ + point_to_affine(point_add(point_one, point_two, curve), curve)}; + JacobianPoint result{}; + const auto bits_one{bignum_bit_length(scalar_one)}; + const auto bits_two{bignum_bit_length(scalar_two)}; + const auto bits{bits_one > bits_two ? bits_one : bits_two}; for (std::size_t index = bits; index > 0; --index) { result = point_double(result, curve); - if (bignum_get_bit(scalar, index - 1)) { - result = point_add(result, point, curve); + const auto bit_one{bignum_get_bit(scalar_one, index - 1)}; + const auto bit_two{bignum_get_bit(scalar_two, index - 1)}; + if (bit_one && bit_two) { + result = point_add_mixed(result, combined, curve); + } else if (bit_one) { + result = point_add_mixed(result, point_one, curve); + } else if (bit_two) { + result = point_add_mixed(result, point_two, curve); } } @@ -182,21 +500,19 @@ inline auto point_scalar_multiply(const Bignum &scalar, inline auto point_affine_x(const JacobianPoint &point, const EllipticCurveParameters &curve) -> Bignum { const auto z_inverse{bignum_mod_inverse(point.z, curve.prime)}; - const auto z_inverse_squared{ - bignum_mod_multiply(z_inverse, z_inverse, curve.prime)}; - return bignum_mod_multiply(point.x, z_inverse_squared, curve.prime); + const auto z_inverse_squared{field_square(z_inverse, curve)}; + return field_mod_multiply(point.x, z_inverse_squared, curve); } // Whether the affine point satisfies y^2 = x^3 + a*x + b (mod p) inline auto point_on_curve(const Bignum &x, const Bignum &y, const EllipticCurveParameters &curve) -> bool { const auto &prime{curve.prime}; - const auto left{bignum_mod_multiply(y, y, prime)}; - const auto x_cubed{ - bignum_mod_multiply(bignum_mod_multiply(x, x, prime), x, prime)}; + const auto left{field_square(y, curve)}; + const auto x_cubed{field_mod_multiply(field_square(x, curve), x, curve)}; const auto right{bignum_mod_add( - bignum_mod_add(x_cubed, - bignum_mod_multiply(curve.coefficient_a, x, prime), prime), + bignum_mod_add(x_cubed, field_mod_multiply(curve.coefficient_a, x, curve), + prime), curve.coefficient_b, prime)}; return bignum_compare(left, right) == 0; } diff --git a/vendor/core/src/core/crypto/crypto_eddsa.h b/vendor/core/src/core/crypto/crypto_eddsa.h new file mode 100644 index 00000000..1ff0da48 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa.h @@ -0,0 +1,448 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_EDDSA_H_ +#define SOURCEMETA_CORE_CRYPTO_EDDSA_H_ + +// Edwards-curve signature verification (Ed25519 and Ed448, RFC 8032 Section 5, +// the pure variants) for the backends without a native EdDSA primitive. Points +// are kept in extended Edwards coordinates, so that the group law is a single +// set of complete formulas shared by both curves. Constant time execution is +// not required, since verification consumes only public inputs + +#include + +#include "crypto_bignum.h" +#include "crypto_shake256.h" + +#include // std::size_t +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +// A point in extended Edwards coordinates (X : Y : Z : T), where the affine +// point is (X / Z, Y / Z) and T = X * Y / Z (RFC 8032 Section 5.1.4) +struct EdwardsPoint { + Bignum x; + Bignum y; + Bignum z; + Bignum t; +}; + +struct EdwardsParameters { + Bignum prime; + Bignum order; + Bignum coefficient_a; + Bignum coefficient_d; + EdwardsPoint base; +}; + +// Interpret the bytes as a little-endian unsigned integer, the encoding EdDSA +// uses throughout (RFC 8032 Section 5.1.2), by reversing into the big-endian +// conversion +inline auto bignum_from_bytes_little_endian(const std::string_view input) + -> Bignum { + const std::string reversed{input.rbegin(), input.rend()}; + return bignum_from_bytes(reversed); +} + +// The complete unified Edwards addition formulas in extended coordinates +// (Hisil, Wong, Carter, and Dawson 2008), which hold for any two points, +// including equal points and the identity, since the curve coefficient is a +// square and d is a non-square modulo p +inline auto edwards_point_add(const EdwardsPoint &left, + const EdwardsPoint &right, + const EdwardsParameters ¶meters) + -> EdwardsPoint { + const auto &prime{parameters.prime}; + const auto a{bignum_mod_multiply(left.x, right.x, prime)}; + const auto b{bignum_mod_multiply(left.y, right.y, prime)}; + const auto c{bignum_mod_multiply( + bignum_mod_multiply(parameters.coefficient_d, left.t, prime), right.t, + prime)}; + const auto d{bignum_mod_multiply(left.z, right.z, prime)}; + const auto e{bignum_mod_subtract( + bignum_mod_multiply(bignum_mod_add(left.x, left.y, prime), + bignum_mod_add(right.x, right.y, prime), prime), + bignum_mod_add(a, b, prime), prime)}; + const auto f{bignum_mod_subtract(d, c, prime)}; + const auto g{bignum_mod_add(d, c, prime)}; + const auto h{bignum_mod_subtract( + b, bignum_mod_multiply(parameters.coefficient_a, a, prime), prime)}; + return EdwardsPoint{.x = bignum_mod_multiply(e, f, prime), + .y = bignum_mod_multiply(g, h, prime), + .z = bignum_mod_multiply(f, g, prime), + .t = bignum_mod_multiply(e, h, prime)}; +} + +inline auto edwards_point_scalar_multiply(const Bignum &scalar, + const EdwardsPoint &point, + const EdwardsParameters ¶meters) + -> EdwardsPoint { + // The identity element is (0 : 1 : 1 : 0) + EdwardsPoint result{.x = Bignum{}, + .y = bignum_from_u64(1), + .z = bignum_from_u64(1), + .t = Bignum{}}; + const auto bits{bignum_bit_length(scalar)}; + for (std::size_t index = bits; index > 0; --index) { + result = edwards_point_add(result, result, parameters); + if (bignum_get_bit(scalar, index - 1)) { + result = edwards_point_add(result, point, parameters); + } + } + + return result; +} + +// Whether two points are equal, compared without leaving projective space by +// cross-multiplying through the Z factors +inline auto edwards_point_equal(const EdwardsPoint &left, + const EdwardsPoint &right, const Bignum &prime) + -> bool { + return bignum_compare(bignum_mod_multiply(left.x, right.z, prime), + bignum_mod_multiply(right.x, left.z, prime)) == 0 && + bignum_compare(bignum_mod_multiply(left.y, right.z, prime), + bignum_mod_multiply(right.y, left.z, prime)) == 0; +} + +// Recover an Ed25519 point from its 32-byte encoding (RFC 8032 Section 5.1.3), +// returning no value when the encoding does not name a point on the curve +inline auto edwards25519_decode_point(const std::string_view encoding, + const Bignum &prime, + const Bignum &coefficient_d, + const Bignum &square_root_of_minus_one) + -> std::optional { + if (encoding.size() != 32) { + return std::nullopt; + } + + // The final bit holds the sign of x, the remaining bits the little-endian y + std::string bytes{encoding}; + const auto sign_bit{ + static_cast(static_cast(bytes.back()) >> 7) & 1u}; + bytes.back() = + static_cast(static_cast(bytes.back()) & 0x7fu); + const auto y{bignum_from_bytes_little_endian(bytes)}; + + // A y coordinate at or beyond the field prime is not a canonical encoding + if (bignum_compare(y, prime) >= 0) { + return std::nullopt; + } + + const auto one{bignum_from_u64(1)}; + const auto y_squared{bignum_mod_multiply(y, y, prime)}; + + // Solve x^2 = (y^2 - 1) / (d * y^2 + 1) (mod p) + const auto numerator{bignum_mod_subtract(y_squared, one, prime)}; + const auto denominator{bignum_mod_add( + bignum_mod_multiply(coefficient_d, y_squared, prime), one, prime)}; + + // The candidate root is x = numerator * denominator^3 * + // (numerator * denominator^7)^((p - 5) / 8) (mod p), a single powering that + // folds in the inversion of the denominator + const auto denominator_squared{ + bignum_mod_multiply(denominator, denominator, prime)}; + const auto denominator_cubed{ + bignum_mod_multiply(denominator_squared, denominator, prime)}; + const auto denominator_seventh{bignum_mod_multiply( + bignum_mod_multiply(denominator_cubed, denominator_cubed, prime), + denominator, prime)}; + auto exponent{prime}; + bignum_subtract_in_place(exponent, bignum_from_u64(5)); + exponent = bignum_shift_right(exponent, 3); + const auto root{ + bignum_mod_exp(bignum_mod_multiply(numerator, denominator_seventh, prime), + exponent, prime)}; + auto candidate{bignum_mod_multiply( + bignum_mod_multiply(numerator, denominator_cubed, prime), root, prime)}; + + // The candidate is correct when denominator * x^2 equals the numerator, off + // by sqrt(-1) when it equals its negation, and otherwise no root exists + const auto check{bignum_mod_multiply( + denominator, bignum_mod_multiply(candidate, candidate, prime), prime)}; + if (bignum_compare(check, numerator) != 0) { + const auto negated_numerator{ + bignum_mod_subtract(Bignum{}, numerator, prime)}; + if (bignum_compare(check, negated_numerator) != 0) { + return std::nullopt; + } + + candidate = bignum_mod_multiply(candidate, square_root_of_minus_one, prime); + } + + // Reject the non-canonical zero root with a set sign bit, then select the + // root whose low bit matches the encoded sign + if (bignum_is_zero(candidate) && sign_bit == 1) { + return std::nullopt; + } + + if (static_cast(bignum_get_bit(candidate, 0)) != sign_bit) { + auto negated{prime}; + bignum_subtract_in_place(negated, candidate); + candidate = negated; + } + + return EdwardsPoint{.x = candidate, + .y = y, + .z = one, + .t = bignum_mod_multiply(candidate, y, prime)}; +} + +// The Edwards25519 domain parameters (RFC 8032 Section 5.1) +inline auto edwards25519() -> EdwardsParameters { + EdwardsParameters parameters; + parameters.prime = bignum_from_hex( + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed"); + parameters.order = bignum_from_hex( + "1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed"); + + // The curve coefficient a is -1 (mod p) + parameters.coefficient_a = parameters.prime; + bignum_subtract_in_place(parameters.coefficient_a, bignum_from_u64(1)); + + // d = -121665 / 121666 (mod p) + auto negated_numerator{parameters.prime}; + bignum_subtract_in_place(negated_numerator, bignum_from_u64(121665)); + parameters.coefficient_d = bignum_mod_multiply( + negated_numerator, + bignum_mod_inverse(bignum_from_u64(121666), parameters.prime), + parameters.prime); + + // sqrt(-1) = 2^((p - 1) / 4) (mod p), used to recover the second root + auto root_exponent{parameters.prime}; + bignum_subtract_in_place(root_exponent, bignum_from_u64(1)); + root_exponent = bignum_shift_right(root_exponent, 2); + const auto square_root_of_minus_one{ + bignum_mod_exp(bignum_from_u64(2), root_exponent, parameters.prime)}; + + // The base point is recovered from its canonical encoding, y = 4/5 with a + // clear sign bit (RFC 8032 Section 5.1) + std::string base_encoding; + base_encoding.push_back('\x58'); + base_encoding.append(31, '\x66'); + parameters.base = edwards25519_decode_point(base_encoding, parameters.prime, + parameters.coefficient_d, + square_root_of_minus_one) + .value(); + return parameters; +} + +// Verify an Ed25519 signature over a message (RFC 8032 Section 5.1.7), given +// the 32-byte public key and the 64-byte signature +inline auto edwards25519_verify(const std::string_view public_key, + const std::string_view message, + const std::string_view signature) -> bool { + if (public_key.size() != 32 || signature.size() != 64) { + return false; + } + + const auto parameters{edwards25519()}; + auto square_root_exponent{parameters.prime}; + bignum_subtract_in_place(square_root_exponent, bignum_from_u64(1)); + square_root_exponent = bignum_shift_right(square_root_exponent, 2); + const auto square_root_of_minus_one{bignum_mod_exp( + bignum_from_u64(2), square_root_exponent, parameters.prime)}; + + const auto public_point{edwards25519_decode_point( + public_key, parameters.prime, parameters.coefficient_d, + square_root_of_minus_one)}; + if (!public_point.has_value()) { + return false; + } + + // The signature is the encoded point R followed by the little-endian scalar + // S, which must lie below the group order + const auto encoded_r{signature.substr(0, 32)}; + const auto point_r{edwards25519_decode_point(encoded_r, parameters.prime, + parameters.coefficient_d, + square_root_of_minus_one)}; + if (!point_r.has_value()) { + return false; + } + + const auto scalar_s{bignum_from_bytes_little_endian(signature.substr(32))}; + if (bignum_compare(scalar_s, parameters.order) >= 0) { + return false; + } + + // k = SHA-512(R || A || M) reduced modulo the group order + std::string preimage; + preimage.reserve(encoded_r.size() + public_key.size() + message.size()); + preimage.append(encoded_r); + preimage.append(public_key); + preimage.append(message); + const auto digest{sha512_digest(preimage)}; + auto scalar_k{bignum_from_bytes_little_endian(std::string_view{ + reinterpret_cast(digest.data()), digest.size()})}; + bignum_reduce(scalar_k, parameters.order); + + // The signature holds when [S]B = R + [k]A + const auto left{ + edwards_point_scalar_multiply(scalar_s, parameters.base, parameters)}; + const auto right{edwards_point_add( + point_r.value(), + edwards_point_scalar_multiply(scalar_k, public_point.value(), parameters), + parameters)}; + return edwards_point_equal(left, right, parameters.prime); +} + +// Recover an Ed448 point from its 57-byte encoding (RFC 8032 Section 5.2.3), +// returning no value when the encoding does not name a point on the curve +inline auto edwards448_decode_point(const std::string_view encoding, + const Bignum &prime, + const Bignum &coefficient_d) + -> std::optional { + if (encoding.size() != 57) { + return std::nullopt; + } + + // The final bit holds the sign of x, the remaining bits the little-endian y + std::string bytes{encoding}; + const auto sign_bit{ + static_cast(static_cast(bytes.back()) >> 7) & 1u}; + bytes.back() = + static_cast(static_cast(bytes.back()) & 0x7fu); + const auto y{bignum_from_bytes_little_endian(bytes)}; + + // A y coordinate at or beyond the field prime is not a canonical encoding + if (bignum_compare(y, prime) >= 0) { + return std::nullopt; + } + + const auto one{bignum_from_u64(1)}; + const auto y_squared{bignum_mod_multiply(y, y, prime)}; + + // Solve x^2 = (y^2 - 1) / (d * y^2 - 1) (mod p) + const auto numerator{bignum_mod_subtract(y_squared, one, prime)}; + const auto denominator{bignum_mod_subtract( + bignum_mod_multiply(coefficient_d, y_squared, prime), one, prime)}; + + // The candidate root is x = numerator^3 * denominator * + // (numerator^5 * denominator^3)^((p - 3) / 4) (mod p), the field having + // p congruent to 3 modulo 4 + const auto numerator_squared{ + bignum_mod_multiply(numerator, numerator, prime)}; + const auto numerator_cubed{ + bignum_mod_multiply(numerator_squared, numerator, prime)}; + const auto numerator_fifth{ + bignum_mod_multiply(numerator_squared, numerator_cubed, prime)}; + const auto denominator_squared{ + bignum_mod_multiply(denominator, denominator, prime)}; + const auto denominator_cubed{ + bignum_mod_multiply(denominator_squared, denominator, prime)}; + auto exponent{prime}; + bignum_subtract_in_place(exponent, bignum_from_u64(3)); + exponent = bignum_shift_right(exponent, 2); + const auto root{bignum_mod_exp( + bignum_mod_multiply(numerator_fifth, denominator_cubed, prime), exponent, + prime)}; + auto candidate{bignum_mod_multiply( + bignum_mod_multiply(numerator_cubed, denominator, prime), root, prime)}; + + // The candidate is correct when denominator * x^2 equals the numerator, and + // otherwise no root exists, as the field admits a single square root + const auto check{bignum_mod_multiply( + denominator, bignum_mod_multiply(candidate, candidate, prime), prime)}; + if (bignum_compare(check, numerator) != 0) { + return std::nullopt; + } + + // Reject the non-canonical zero root with a set sign bit, then select the + // root whose low bit matches the encoded sign + if (bignum_is_zero(candidate) && sign_bit == 1) { + return std::nullopt; + } + + if (static_cast(bignum_get_bit(candidate, 0)) != sign_bit) { + auto negated{prime}; + bignum_subtract_in_place(negated, candidate); + candidate = negated; + } + + return EdwardsPoint{.x = candidate, + .y = y, + .z = one, + .t = bignum_mod_multiply(candidate, y, prime)}; +} + +// The Edwards448 domain parameters (RFC 8032 Section 5.2) +inline auto edwards448() -> EdwardsParameters { + EdwardsParameters parameters; + // clang-format off + parameters.prime = bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + parameters.order = bignum_from_hex("3fffffffffffffffffffffffffffffffffffffffffffffffffffffff7cca23e9c44edb49aed63690216cc2728dc58f552378c292ab5844f3"); + // clang-format on + + // The curve coefficient a is 1, and d is -39081 (mod p) + parameters.coefficient_a = bignum_from_u64(1); + parameters.coefficient_d = parameters.prime; + bignum_subtract_in_place(parameters.coefficient_d, bignum_from_u64(39081)); + + // The base point is recovered from its canonical 57-octet encoding (RFC 8032 + // Section 5.2) + // clang-format off + const auto base_encoding{bignum_to_bytes(bignum_from_hex("14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900"), 57)}; + // clang-format on + parameters.base = edwards448_decode_point(base_encoding, parameters.prime, + parameters.coefficient_d) + .value(); + return parameters; +} + +// Verify an Ed448 signature over a message (RFC 8032 Section 5.2.7), given the +// 57-byte public key and the 114-byte signature +inline auto edwards448_verify(const std::string_view public_key, + const std::string_view message, + const std::string_view signature) -> bool { + if (public_key.size() != 57 || signature.size() != 114) { + return false; + } + + const auto parameters{edwards448()}; + const auto public_point{edwards448_decode_point(public_key, parameters.prime, + parameters.coefficient_d)}; + if (!public_point.has_value()) { + return false; + } + + // The signature is the encoded point R followed by the little-endian scalar + // S, which must lie below the group order + const auto encoded_r{signature.substr(0, 57)}; + const auto point_r{edwards448_decode_point(encoded_r, parameters.prime, + parameters.coefficient_d)}; + if (!point_r.has_value()) { + return false; + } + + const auto scalar_s{bignum_from_bytes_little_endian(signature.substr(57))}; + if (bignum_compare(scalar_s, parameters.order) >= 0) { + return false; + } + + // k = SHAKE256(dom4 || R || A || M) reduced modulo the group order, where + // dom4 is "SigEd448" followed by the zero pre-hash flag and an empty context + // (RFC 8032 Section 5.2.7 and Section 2) + std::string preimage{"SigEd448"}; + preimage.push_back('\x00'); + preimage.push_back('\x00'); + preimage.append(encoded_r); + preimage.append(public_key); + preimage.append(message); + const auto digest{shake256(preimage, 114)}; + auto scalar_k{bignum_from_bytes_little_endian(digest)}; + bignum_reduce(scalar_k, parameters.order); + + // The signature holds when [S]B = R + [k]A + const auto left{ + edwards_point_scalar_multiply(scalar_s, parameters.base, parameters)}; + const auto right{edwards_point_add( + point_r.value(), + edwards_point_scalar_multiply(scalar_k, public_point.value(), parameters), + parameters)}; + return edwards_point_equal(left, right, parameters.prime); +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_eddsa_apple.h b/vendor/core/src/core/crypto/crypto_eddsa_apple.h new file mode 100644 index 00000000..7426649a --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa_apple.h @@ -0,0 +1,15 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_EDDSA_APPLE_H_ +#define SOURCEMETA_CORE_CRYPTO_EDDSA_APPLE_H_ + +#include // std::size_t + +// Verify an Ed25519 signature through CryptoKit, defined in the Objective-C++ +// bridge that consumes the Swift shim. The signature is invalid rather than an +// error for any malformed input, including a key or signature of the wrong +// length, since CryptoKit rejects those inputs +extern "C" auto sourcemeta_core_eddsa_ed25519_verify_cryptokit( + const unsigned char *public_key, std::size_t public_key_size, + const unsigned char *message, std::size_t message_size, + const unsigned char *signature, std::size_t signature_size) -> bool; + +#endif diff --git a/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm new file mode 100644 index 00000000..814b6b50 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm @@ -0,0 +1,20 @@ +#include "crypto_eddsa_apple.h" + +#import // NSData + +// The Objective-C interface generated from the Swift shim +#import "sourcemeta_core_cryptokit-Swift.h" + +extern "C" auto sourcemeta_core_eddsa_ed25519_verify_cryptokit( + const unsigned char *public_key, std::size_t public_key_size, + const unsigned char *message, std::size_t message_size, + const unsigned char *signature, std::size_t signature_size) -> bool { + @autoreleasepool { + NSData *const key{[NSData dataWithBytes:public_key length:public_key_size]}; + NSData *const payload{[NSData dataWithBytes:message length:message_size]}; + NSData *const tag{[NSData dataWithBytes:signature length:signature_size]}; + return [SourcemetaCoreEd25519 verifyWithPublicKey:key + message:payload + signature:tag] == YES; + } +} diff --git a/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift new file mode 100644 index 00000000..0ab74c74 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift @@ -0,0 +1,19 @@ +import CryptoKit +import Foundation + +// The Ed25519 verification primitive that the Apple Security framework does +// not expose through its C API. CryptoKit provides it since macOS 10.15, and +// this class surfaces it to the Objective-C++ bridge through the generated +// Objective-C interface header +@objc(SourcemetaCoreEd25519) +public final class SourcemetaCoreEd25519: NSObject { + @objc public static func verify(publicKey: Data, message: Data, + signature: Data) -> Bool { + guard let key = try? Curve25519.Signing.PublicKey( + rawRepresentation: publicKey) else { + return false + } + + return key.isValidSignature(signature, for: message) + } +} diff --git a/vendor/core/src/core/crypto/crypto_helpers.h b/vendor/core/src/core/crypto/crypto_helpers.h index 645b4d96..e1751d46 100644 --- a/vendor/core/src/core/crypto/crypto_helpers.h +++ b/vendor/core/src/core/crypto/crypto_helpers.h @@ -5,6 +5,7 @@ #include #include #include +#include #include // std::size_t #include // std::string @@ -17,6 +18,22 @@ namespace sourcemeta::core { // the range of valid key sizes inline constexpr std::size_t MAXIMUM_KEY_BYTES{512}; +// Whether a signature representative, as a big-endian integer, is strictly +// less than the modulus. RFC 8017 Section 5.2.2 requires this range check, so +// that an unreduced signature, which an attacker forges by adding the modulus +// without changing the modular exponentiation result, is rejected +inline auto rsa_signature_in_range(const std::string_view signature, + const std::string_view modulus) noexcept + -> bool { + const auto value{strip_left(signature, '\x00')}; + const auto bound{strip_left(modulus, '\x00')}; + if (value.size() != bound.size()) { + return value.size() < bound.size(); + } + + return value < bound; +} + inline auto curve_field_bytes(const EllipticCurve curve) noexcept -> std::size_t { switch (curve) { @@ -31,6 +48,32 @@ inline auto curve_field_bytes(const EllipticCurve curve) noexcept std::unreachable(); } +// The public key and signature octet lengths are fixed per curve (RFC 8032 +// Section 5.1.2 and Section 5.1.6) +inline auto eddsa_public_key_bytes(const EdwardsCurve curve) noexcept + -> std::size_t { + switch (curve) { + case EdwardsCurve::Ed25519: + return 32; + case EdwardsCurve::Ed448: + return 57; + } + + std::unreachable(); +} + +inline auto eddsa_signature_bytes(const EdwardsCurve curve) noexcept + -> std::size_t { + switch (curve) { + case EdwardsCurve::Ed25519: + return 64; + case EdwardsCurve::Ed448: + return 114; + } + + std::unreachable(); +} + inline auto digest_message(const SignatureHashFunction hash, const std::string_view message) -> std::string { switch (hash) { diff --git a/vendor/core/src/core/crypto/crypto_shake256.h b/vendor/core/src/core/crypto/crypto_shake256.h new file mode 100644 index 00000000..dd942542 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_shake256.h @@ -0,0 +1,131 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_SHAKE256_H_ +#define SOURCEMETA_CORE_CRYPTO_SHAKE256_H_ + +// The SHAKE256 extendable-output function (FIPS 202), built on the Keccak-f +// [1600] permutation, for the reference Ed448 verification backend + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint64_t +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +inline constexpr std::array keccak_round_constants{ + {0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, + 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL}}; + +// The rotation offsets and destination lanes of the combined rho and pi steps, +// walking the lanes starting from lane 1 +inline constexpr std::array keccak_rho_offsets{ + {1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, + 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44}}; + +inline constexpr std::array keccak_pi_lanes{ + {10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, + 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1}}; + +inline constexpr auto keccak_rotate_left(const std::uint64_t value, + const unsigned offset) noexcept + -> std::uint64_t { + return (value << offset) | (value >> (64u - offset)); +} + +inline auto keccak_permute(std::array &state) noexcept + -> void { + for (std::size_t round = 0; round < 24; ++round) { + // Theta + std::array column_parity{}; + for (std::size_t column = 0; column < 5; ++column) { + column_parity[column] = state[column] ^ state[column + 5] ^ + state[column + 10] ^ state[column + 15] ^ + state[column + 20]; + } + + for (std::size_t column = 0; column < 5; ++column) { + const auto delta{column_parity[(column + 4) % 5] ^ + keccak_rotate_left(column_parity[(column + 1) % 5], 1)}; + for (std::size_t row = 0; row < 25; row += 5) { + state[row + column] ^= delta; + } + } + + // Rho and pi + auto current{state[1]}; + for (std::size_t index = 0; index < 24; ++index) { + const auto lane{keccak_pi_lanes[index]}; + const auto moved{state[lane]}; + state[lane] = keccak_rotate_left(current, keccak_rho_offsets[index]); + current = moved; + } + + // Chi + for (std::size_t row = 0; row < 25; row += 5) { + std::array plane{}; + for (std::size_t column = 0; column < 5; ++column) { + plane[column] = state[row + column]; + } + + for (std::size_t column = 0; column < 5; ++column) { + state[row + column] = plane[column] ^ (~plane[(column + 1) % 5] & + plane[(column + 2) % 5]); + } + } + + // Iota + state[0] ^= keccak_round_constants[round]; + } +} + +// Hash a string with SHAKE256, returning the requested number of output bytes +inline auto shake256(const std::string_view input, + const std::size_t output_length) -> std::string { + // The bitrate is 1600 - 2 * 256 = 1088 bits, that is 136 octets + constexpr std::size_t rate{136}; + std::array state{}; + + std::size_t pointer{0}; + for (const auto character : input) { + state[pointer / 8] ^= + static_cast(static_cast(character)) + << (8 * (pointer % 8)); + pointer += 1; + if (pointer == rate) { + keccak_permute(state); + pointer = 0; + } + } + + // The SHAKE domain separation suffix is the bits 1111, padded to the rate + // with the pad10*1 rule + state[pointer / 8] ^= static_cast(0x1f) << (8 * (pointer % 8)); + state[(rate - 1) / 8] ^= static_cast(0x80) + << (8 * ((rate - 1) % 8)); + keccak_permute(state); + + std::string output; + output.reserve(output_length); + std::size_t squeeze_pointer{0}; + while (output.size() < output_length) { + output.push_back(static_cast( + (state[squeeze_pointer / 8] >> (8 * (squeeze_pointer % 8))) & 0xffu)); + squeeze_pointer += 1; + if (squeeze_pointer == rate) { + keccak_permute(state); + squeeze_pointer = 0; + } + } + + return output; +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_verify_apple.cc b/vendor/core/src/core/crypto/crypto_verify_apple.cc new file mode 100644 index 00000000..7f116b0f --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_apple.cc @@ -0,0 +1,368 @@ +#include +#include + +#include "crypto_eddsa.h" +#include "crypto_eddsa_apple.h" +#include "crypto_helpers.h" + +#include // CF*, kCF* +#include // Sec*, kSec* + +#include // std::array +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { + +// The parsed key keeps the platform key object alive for reuse. The Edwards +// curves have no Security framework primitive, so they keep the raw encoded +// point and verify through CryptoKit or the reference implementation +struct PublicKey::Internal { + PublicKey::Type kind; + SecKeyRef key; + std::string modulus; + std::size_t field_bytes; + std::string edwards_point; + EdwardsCurve edwards_curve; +}; + +} // namespace sourcemeta::core + +namespace { + +auto to_sec_key_pkcs1_v15_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> SecKeyAlgorithm { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256; + case sourcemeta::core::SignatureHashFunction::SHA384: + return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA384; + case sourcemeta::core::SignatureHashFunction::SHA512: + return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA512; + } + + std::unreachable(); +} + +// These algorithm variants fix the salt length to the hash function output, +// which is exactly what RFC 7518 Section 3.5 requires +auto to_sec_key_pss_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> SecKeyAlgorithm { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return kSecKeyAlgorithmRSASignatureMessagePSSSHA256; + case sourcemeta::core::SignatureHashFunction::SHA384: + return kSecKeyAlgorithmRSASignatureMessagePSSSHA384; + case sourcemeta::core::SignatureHashFunction::SHA512: + return kSecKeyAlgorithmRSASignatureMessagePSSSHA512; + } + + std::unreachable(); +} + +auto to_sec_key_ecdsa_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> SecKeyAlgorithm { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return kSecKeyAlgorithmECDSASignatureMessageX962SHA256; + case sourcemeta::core::SignatureHashFunction::SHA384: + return kSecKeyAlgorithmECDSASignatureMessageX962SHA384; + case sourcemeta::core::SignatureHashFunction::SHA512: + return kSecKeyAlgorithmECDSASignatureMessageX962SHA512; + } + + std::unreachable(); +} + +auto make_data(const std::string_view value) -> CFDataRef { + return CFDataCreate(kCFAllocatorDefault, + reinterpret_cast(value.data()), + static_cast(value.size())); +} + +auto native_rsa_key(const std::string_view modulus, + const std::string_view exponent) -> SecKeyRef { + // The platform expects the PKCS#1 RSAPublicKey structure, a DER sequence of + // the modulus and exponent integers (RFC 8017 Appendix A.1.1) + std::string body; + sourcemeta::core::der_append_unsigned_integer(body, modulus); + sourcemeta::core::der_append_unsigned_integer(body, exponent); + std::string der; + der.push_back('\x30'); + sourcemeta::core::der_append_length(der, body.size()); + der.append(body); + + auto key_data{make_data(der)}; + if (key_data == nullptr) { + return nullptr; + } + + std::array attribute_keys{ + {kSecAttrKeyType, kSecAttrKeyClass}}; + std::array attribute_values{ + {kSecAttrKeyTypeRSA, kSecAttrKeyClassPublic}}; + auto attributes{CFDictionaryCreate( + kCFAllocatorDefault, attribute_keys.data(), attribute_values.data(), + static_cast(attribute_keys.size()), + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)}; + if (attributes == nullptr) { + CFRelease(key_data); + return nullptr; + } + + auto key{SecKeyCreateWithData(key_data, attributes, nullptr)}; + CFRelease(attributes); + CFRelease(key_data); + return key; +} + +auto native_ec_key(const sourcemeta::core::EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) -> SecKeyRef { + const auto width{sourcemeta::core::curve_field_bytes(curve)}; + const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; + const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > width || stripped_y.size() > width) { + return nullptr; + } + + // The platform infers the curve from the X9.63 uncompressed point, the 0x04 + // lead byte followed by the two fixed-width coordinates + std::string point; + point.push_back('\x04'); + point.append(sourcemeta::core::pad_left(stripped_x, width, '\x00')); + point.append(sourcemeta::core::pad_left(stripped_y, width, '\x00')); + + auto key_data{make_data(point)}; + if (key_data == nullptr) { + return nullptr; + } + + std::array attribute_keys{ + {kSecAttrKeyType, kSecAttrKeyClass}}; + std::array attribute_values{ + {kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClassPublic}}; + auto attributes{CFDictionaryCreate( + kCFAllocatorDefault, attribute_keys.data(), attribute_values.data(), + static_cast(attribute_keys.size()), + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)}; + if (attributes == nullptr) { + CFRelease(key_data); + return nullptr; + } + + auto key{SecKeyCreateWithData(key_data, attributes, nullptr)}; + CFRelease(attributes); + CFRelease(key_data); + return key; +} + +auto encode_ecdsa_signature(const std::string_view raw_signature) + -> std::string { + // The raw form is the two integers concatenated, while the platform expects + // the X9.62 DER sequence of those integers + const auto half{raw_signature.size() / 2}; + std::string body; + sourcemeta::core::der_append_unsigned_integer(body, + raw_signature.substr(0, half)); + sourcemeta::core::der_append_unsigned_integer(body, + raw_signature.substr(half)); + std::string der; + der.push_back('\x30'); + sourcemeta::core::der_append_length(der, body.size()); + der.append(body); + return der; +} + +auto verify_with_algorithm(SecKeyRef key, const SecKeyAlgorithm algorithm, + const std::string_view message, + const std::string_view signature) -> bool { + auto message_data{make_data(message)}; + auto signature_data{make_data(signature)}; + auto result{false}; + if (message_data != nullptr && signature_data != nullptr) { + result = SecKeyVerifySignature(key, algorithm, message_data, signature_data, + nullptr) == true; + } + + if (signature_data != nullptr) { + CFRelease(signature_data); + } + + if (message_data != nullptr) { + CFRelease(message_data); + } + + return result; +} + +} // namespace + +namespace sourcemeta::core { + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + CFRelease(internal_->key); + } + + delete internal_; + } +} + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + CFRelease(internal_->key); + } + + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { return internal_->kind; } + +auto make_rsa_public_key(const std::string_view modulus, + const std::string_view exponent) + -> std::optional { + auto stripped_modulus{std::string{strip_left(modulus, '\x00')}}; + const auto stripped_exponent{strip_left(exponent, '\x00')}; + if (stripped_modulus.empty() || stripped_exponent.empty() || + stripped_modulus.size() > MAXIMUM_KEY_BYTES || + stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + return std::nullopt; + } + + auto *key{native_rsa_key(stripped_modulus, stripped_exponent)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .key = key, + .modulus = std::move(stripped_modulus), + .field_bytes = 0, + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + auto *key{native_ec_key(curve, coordinate_x, coordinate_y)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .key = key, + .modulus = {}, + .field_bytes = curve_field_bytes(curve), + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .key = nullptr, + .modulus = {}, + .field_bytes = 0, + .edwards_point = std::string{public_key}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_with_algorithm( + internal->key, to_sec_key_pkcs1_v15_algorithm(hash), message, signature); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_with_algorithm(internal->key, to_sec_key_pss_algorithm(hash), + message, signature); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::EllipticCurve || + signature.size() != internal->field_bytes * 2) { + return false; + } + + return verify_with_algorithm(internal->key, to_sec_key_ecdsa_algorithm(hash), + message, encode_ecdsa_signature(signature)); +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards || + signature.size() != eddsa_signature_bytes(internal->edwards_curve)) { + return false; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: + return sourcemeta_core_eddsa_ed25519_verify_cryptokit( + reinterpret_cast( + internal->edwards_point.data()), + internal->edwards_point.size(), + reinterpret_cast(message.data()), + message.size(), + reinterpret_cast(signature.data()), + signature.size()); + case EdwardsCurve::Ed448: + return edwards448_verify(internal->edwards_point, message, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_ecdsa_apple.cc b/vendor/core/src/core/crypto/crypto_verify_ecdsa_apple.cc deleted file mode 100644 index 5a23aba1..00000000 --- a/vendor/core/src/core/crypto/crypto_verify_ecdsa_apple.cc +++ /dev/null @@ -1,135 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#include // CF*, kCF* -#include // Sec*, kSec* - -#include // std::array -#include // std::string -#include // std::string_view -#include // std::unreachable - -namespace { - -auto to_sec_key_ecdsa_algorithm( - const sourcemeta::core::SignatureHashFunction hash) noexcept - -> SecKeyAlgorithm { - switch (hash) { - case sourcemeta::core::SignatureHashFunction::SHA256: - return kSecKeyAlgorithmECDSASignatureMessageX962SHA256; - case sourcemeta::core::SignatureHashFunction::SHA384: - return kSecKeyAlgorithmECDSASignatureMessageX962SHA384; - case sourcemeta::core::SignatureHashFunction::SHA512: - return kSecKeyAlgorithmECDSASignatureMessageX962SHA512; - } - - std::unreachable(); -} - -auto make_data(const std::string_view value) -> CFDataRef { - return CFDataCreate(kCFAllocatorDefault, - reinterpret_cast(value.data()), - static_cast(value.size())); -} - -auto make_ec_public_key(const sourcemeta::core::EllipticCurve curve, - const std::string_view coordinate_x, - const std::string_view coordinate_y) -> SecKeyRef { - const auto width{sourcemeta::core::curve_field_bytes(curve)}; - const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; - const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; - if (stripped_x.size() > width || stripped_y.size() > width) { - return nullptr; - } - - // The platform infers the curve from the X9.63 uncompressed point, the - // 0x04 lead byte followed by the two fixed-width coordinates - std::string point; - point.push_back('\x04'); - point.append(sourcemeta::core::pad_left(stripped_x, width, '\x00')); - point.append(sourcemeta::core::pad_left(stripped_y, width, '\x00')); - - auto key_data{make_data(point)}; - if (key_data == nullptr) { - return nullptr; - } - - std::array attribute_keys{ - {kSecAttrKeyType, kSecAttrKeyClass}}; - std::array attribute_values{ - {kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClassPublic}}; - auto attributes{CFDictionaryCreate( - kCFAllocatorDefault, attribute_keys.data(), attribute_values.data(), - static_cast(attribute_keys.size()), - &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)}; - if (attributes == nullptr) { - CFRelease(key_data); - return nullptr; - } - - auto key{SecKeyCreateWithData(key_data, attributes, nullptr)}; - CFRelease(attributes); - CFRelease(key_data); - return key; -} - -auto encode_ecdsa_signature(const std::string_view raw_signature) - -> std::string { - // The raw form is the two integers concatenated, while the platform - // expects the X9.62 DER sequence of those integers - const auto half{raw_signature.size() / 2}; - std::string body; - sourcemeta::core::der_append_unsigned_integer(body, - raw_signature.substr(0, half)); - sourcemeta::core::der_append_unsigned_integer(body, - raw_signature.substr(half)); - std::string der; - der.push_back('\x30'); - sourcemeta::core::der_append_length(der, body.size()); - der.append(body); - return der; -} - -} // namespace - -namespace sourcemeta::core { - -auto ecdsa_verify(const EllipticCurve curve, const SignatureHashFunction hash, - const std::string_view coordinate_x, - const std::string_view coordinate_y, - const std::string_view message, - const std::string_view signature) -> bool { - if (signature.size() != sourcemeta::core::curve_field_bytes(curve) * 2) { - return false; - } - - auto key{make_ec_public_key(curve, coordinate_x, coordinate_y)}; - if (key == nullptr) { - return false; - } - - const auto der_signature{encode_ecdsa_signature(signature)}; - auto message_data{make_data(message)}; - auto signature_data{make_data(der_signature)}; - auto result{false}; - if (message_data != nullptr && signature_data != nullptr) { - result = - SecKeyVerifySignature(key, to_sec_key_ecdsa_algorithm(hash), - message_data, signature_data, nullptr) == true; - } - - if (signature_data != nullptr) { - CFRelease(signature_data); - } - - if (message_data != nullptr) { - CFRelease(message_data); - } - - CFRelease(key); - return result; -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_ecdsa_openssl.cc b/vendor/core/src/core/crypto/crypto_verify_ecdsa_openssl.cc deleted file mode 100644 index f9741721..00000000 --- a/vendor/core/src/core/crypto/crypto_verify_ecdsa_openssl.cc +++ /dev/null @@ -1,161 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#include // BN_* -#include // OSSL_PKEY_PARAM_* -#include // ECDSA_SIG_*, i2d_ECDSA_SIG -#include // EVP_* -#include // OSSL_PARAM_* - -#include // std::size_t -#include // std::string -#include // std::string_view -#include // std::unreachable - -namespace { - -auto to_message_digest( - const sourcemeta::core::SignatureHashFunction hash) noexcept - -> const EVP_MD * { - switch (hash) { - case sourcemeta::core::SignatureHashFunction::SHA256: - return EVP_sha256(); - case sourcemeta::core::SignatureHashFunction::SHA384: - return EVP_sha384(); - case sourcemeta::core::SignatureHashFunction::SHA512: - return EVP_sha512(); - } - - std::unreachable(); -} - -auto to_group_name(const sourcemeta::core::EllipticCurve curve) noexcept - -> const char * { - switch (curve) { - case sourcemeta::core::EllipticCurve::P256: - return "P-256"; - case sourcemeta::core::EllipticCurve::P384: - return "P-384"; - case sourcemeta::core::EllipticCurve::P521: - return "P-521"; - } - - std::unreachable(); -} - -auto make_ec_public_key(const sourcemeta::core::EllipticCurve curve, - const std::string_view coordinate_x, - const std::string_view coordinate_y) -> EVP_PKEY * { - const auto width{sourcemeta::core::curve_field_bytes(curve)}; - const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; - const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; - if (stripped_x.size() > width || stripped_y.size() > width) { - return nullptr; - } - - std::string point; - point.push_back('\x04'); - point.append(sourcemeta::core::pad_left(stripped_x, width, '\x00')); - point.append(sourcemeta::core::pad_left(stripped_y, width, '\x00')); - - EVP_PKEY *result{nullptr}; - auto *builder{OSSL_PARAM_BLD_new()}; - if (builder != nullptr && - OSSL_PARAM_BLD_push_utf8_string(builder, OSSL_PKEY_PARAM_GROUP_NAME, - to_group_name(curve), 0) == 1 && - OSSL_PARAM_BLD_push_octet_string( - builder, OSSL_PKEY_PARAM_PUB_KEY, - reinterpret_cast(point.data()), - point.size()) == 1) { - auto *parameters{OSSL_PARAM_BLD_to_param(builder)}; - if (parameters != nullptr) { - auto *context{EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr)}; - if (context != nullptr) { - if (EVP_PKEY_fromdata_init(context) == 1) { - EVP_PKEY_fromdata(context, &result, EVP_PKEY_PUBLIC_KEY, parameters); - } - - EVP_PKEY_CTX_free(context); - } - - OSSL_PARAM_free(parameters); - } - } - - OSSL_PARAM_BLD_free(builder); - return result; -} - -// Convert the raw fixed-width R || S concatenation into the DER signature -// that the verification interface expects -auto encode_ecdsa_signature(const std::string_view raw_signature, - unsigned char **output) -> int { - const auto half{raw_signature.size() / 2}; - auto *signature{ECDSA_SIG_new()}; - if (signature == nullptr) { - return -1; - } - - auto *r{ - BN_bin2bn(reinterpret_cast(raw_signature.data()), - static_cast(half), nullptr)}; - auto *s{BN_bin2bn( - reinterpret_cast(raw_signature.data() + half), - static_cast(half), nullptr)}; - if (r == nullptr || s == nullptr || ECDSA_SIG_set0(signature, r, s) != 1) { - BN_free(r); - BN_free(s); - ECDSA_SIG_free(signature); - return -1; - } - - const auto length{i2d_ECDSA_SIG(signature, output)}; - ECDSA_SIG_free(signature); - return length; -} - -} // namespace - -namespace sourcemeta::core { - -auto ecdsa_verify(const EllipticCurve curve, const SignatureHashFunction hash, - const std::string_view coordinate_x, - const std::string_view coordinate_y, - const std::string_view message, - const std::string_view signature) -> bool { - if (signature.size() != sourcemeta::core::curve_field_bytes(curve) * 2) { - return false; - } - - auto *key{make_ec_public_key(curve, coordinate_x, coordinate_y)}; - if (key == nullptr) { - return false; - } - - unsigned char *der_signature{nullptr}; - const auto der_length{encode_ecdsa_signature(signature, &der_signature)}; - auto result{false}; - if (der_length > 0) { - auto *context{EVP_MD_CTX_new()}; - if (context != nullptr) { - if (EVP_DigestVerifyInit(context, nullptr, to_message_digest(hash), - nullptr, key) == 1) { - result = - EVP_DigestVerify( - context, der_signature, static_cast(der_length), - reinterpret_cast(message.data()), - message.size()) == 1; - } - - EVP_MD_CTX_free(context); - } - } - - OPENSSL_free(der_signature); - EVP_PKEY_free(key); - return result; -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_ecdsa_other.cc b/vendor/core/src/core/crypto/crypto_verify_ecdsa_other.cc deleted file mode 100644 index df2a428a..00000000 --- a/vendor/core/src/core/crypto/crypto_verify_ecdsa_other.cc +++ /dev/null @@ -1,119 +0,0 @@ -#include -#include - -#include "crypto_bignum.h" -#include "crypto_ecc.h" -#include "crypto_helpers.h" - -#include // std::size_t -#include // std::string -#include // std::string_view -#include // std::unreachable - -namespace { - -auto to_curve_parameters(const sourcemeta::core::EllipticCurve curve) - -> sourcemeta::core::EllipticCurveParameters { - switch (curve) { - case sourcemeta::core::EllipticCurve::P256: - return sourcemeta::core::curve_p256(); - case sourcemeta::core::EllipticCurve::P384: - return sourcemeta::core::curve_p384(); - case sourcemeta::core::EllipticCurve::P521: - return sourcemeta::core::curve_p521(); - } - - std::unreachable(); -} - -// FIPS 186-4 Section 6.4 step 2, deriving the integer e from the leftmost -// bits of the message digest, truncated to the bit length of the order -auto digest_to_integer(const sourcemeta::core::SignatureHashFunction hash, - const std::string_view message, - const std::size_t order_bits) - -> sourcemeta::core::Bignum { - const auto digest{sourcemeta::core::digest_message(hash, message)}; - auto value{sourcemeta::core::bignum_from_bytes(digest)}; - const auto digest_bits{digest.size() * 8}; - if (digest_bits > order_bits) { - value = - sourcemeta::core::bignum_shift_right(value, digest_bits - order_bits); - } - - return value; -} - -} // namespace - -namespace sourcemeta::core { - -auto ecdsa_verify(const EllipticCurve curve, const SignatureHashFunction hash, - const std::string_view coordinate_x, - const std::string_view coordinate_y, - const std::string_view message, - const std::string_view signature) -> bool { - const auto parameters{to_curve_parameters(curve)}; - const auto field_bytes{parameters.field_bytes}; - - // RFC 7518 Section 3.4: the signature is the fixed-width concatenation of - // the two integers, each as long as the curve field - if (signature.size() != field_bytes * 2) { - return false; - } - - const auto r{bignum_from_bytes(signature.substr(0, field_bytes))}; - const auto s{bignum_from_bytes(signature.substr(field_bytes))}; - - // FIPS 186-4 Section 6.4.2 step 1: both integers must lie in [1, n - 1] - if (bignum_is_zero(r) || bignum_compare(r, parameters.order) >= 0 || - bignum_is_zero(s) || bignum_compare(s, parameters.order) >= 0) { - return false; - } - - // Reject coordinates wider than the field, matching the platform backends - // and preventing an oversized input from being truncated into a valid key - const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; - const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; - if (stripped_x.size() > field_bytes || stripped_y.size() > field_bytes) { - return false; - } - - const auto public_x{bignum_from_bytes(stripped_x)}; - const auto public_y{bignum_from_bytes(stripped_y)}; - - // The public key must be a valid point: coordinates below the field prime - // and satisfying the curve equation - if (bignum_compare(public_x, parameters.prime) >= 0 || - bignum_compare(public_y, parameters.prime) >= 0 || - !point_on_curve(public_x, public_y, parameters)) { - return false; - } - - const auto order_bits{bignum_bit_length(parameters.order)}; - const auto digest_integer{digest_to_integer(hash, message, order_bits)}; - const auto s_inverse{bignum_mod_inverse(s, parameters.order)}; - const auto u1{ - bignum_mod_multiply(digest_integer, s_inverse, parameters.order)}; - const auto u2{bignum_mod_multiply(r, s_inverse, parameters.order)}; - - const JacobianPoint generator{parameters.generator_x, parameters.generator_y, - bignum_from_u64(1)}; - const JacobianPoint public_point{public_x, public_y, bignum_from_u64(1)}; - const auto point{point_add( - point_scalar_multiply(u1, generator, parameters), - point_scalar_multiply(u2, public_point, parameters), parameters)}; - - // FIPS 186-4 Section 6.4.2 step 6: reject when the combination is the - // point at infinity - if (point_is_infinity(point)) { - return false; - } - - // FIPS 186-4 Section 6.4.2 step 7: the signature is valid when the affine - // x coordinate, reduced modulo the order, equals r - auto candidate{point_affine_x(point, parameters)}; - bignum_reduce(candidate, parameters.order); - return bignum_compare(candidate, r) == 0; -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_ecdsa_windows.cc b/vendor/core/src/core/crypto/crypto_verify_ecdsa_windows.cc deleted file mode 100644 index 6654099d..00000000 --- a/vendor/core/src/core/crypto/crypto_verify_ecdsa_windows.cc +++ /dev/null @@ -1,107 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include // ULONG, LPCWSTR - -#include // BCrypt*, BCRYPT_* - -#include // std::memcpy -#include // std::string -#include // std::string_view -#include // std::unreachable - -namespace { - -auto to_ecdsa_algorithm(const sourcemeta::core::EllipticCurve curve) noexcept - -> LPCWSTR { - switch (curve) { - case sourcemeta::core::EllipticCurve::P256: - return BCRYPT_ECDSA_P256_ALGORITHM; - case sourcemeta::core::EllipticCurve::P384: - return BCRYPT_ECDSA_P384_ALGORITHM; - case sourcemeta::core::EllipticCurve::P521: - return BCRYPT_ECDSA_P521_ALGORITHM; - } - - std::unreachable(); -} - -auto to_ecc_public_magic(const sourcemeta::core::EllipticCurve curve) noexcept - -> ULONG { - switch (curve) { - case sourcemeta::core::EllipticCurve::P256: - return BCRYPT_ECDSA_PUBLIC_P256_MAGIC; - case sourcemeta::core::EllipticCurve::P384: - return BCRYPT_ECDSA_PUBLIC_P384_MAGIC; - case sourcemeta::core::EllipticCurve::P521: - return BCRYPT_ECDSA_PUBLIC_P521_MAGIC; - } - - std::unreachable(); -} - -} // namespace - -namespace sourcemeta::core { - -auto ecdsa_verify(const EllipticCurve curve, const SignatureHashFunction hash, - const std::string_view coordinate_x, - const std::string_view coordinate_y, - const std::string_view message, - const std::string_view signature) -> bool { - const auto width{sourcemeta::core::curve_field_bytes(curve)}; - const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; - const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; - if (signature.size() != width * 2 || stripped_x.size() > width || - stripped_y.size() > width) { - return false; - } - - // The public key blob is the header followed by the two fixed-width - // coordinates - BCRYPT_ECCKEY_BLOB header{}; - header.dwMagic = to_ecc_public_magic(curve); - header.cbKey = static_cast(width); - - std::string blob; - blob.resize(sizeof(header)); - std::memcpy(blob.data(), &header, sizeof(header)); - blob.append(sourcemeta::core::pad_left(stripped_x, width, '\x00')); - blob.append(sourcemeta::core::pad_left(stripped_y, width, '\x00')); - - BCRYPT_ALG_HANDLE algorithm{nullptr}; - if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( - &algorithm, to_ecdsa_algorithm(curve), nullptr, 0))) { - return false; - } - - BCRYPT_KEY_HANDLE key{nullptr}; - if (!BCRYPT_SUCCESS( - BCryptImportKeyPair(algorithm, nullptr, BCRYPT_ECCPUBLIC_BLOB, &key, - reinterpret_cast(blob.data()), - static_cast(blob.size()), 0))) { - BCryptCloseAlgorithmProvider(algorithm, 0); - return false; - } - - const auto digest{sourcemeta::core::digest_message(hash, message)}; - - // The CNG signature format is the raw fixed-width R || S concatenation, so - // the input passes through unchanged - const auto result{BCRYPT_SUCCESS(BCryptVerifySignature( - key, nullptr, - reinterpret_cast(const_cast(digest.data())), - static_cast(digest.size()), - reinterpret_cast(const_cast(signature.data())), - static_cast(signature.size()), 0))}; - - BCryptDestroyKey(key); - BCryptCloseAlgorithmProvider(algorithm, 0); - return result; -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_openssl.cc b/vendor/core/src/core/crypto/crypto_verify_openssl.cc new file mode 100644 index 00000000..428ba2f1 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_openssl.cc @@ -0,0 +1,404 @@ +#include +#include + +#include "crypto_helpers.h" + +#include // BN_* +#include // OSSL_PKEY_PARAM_* +#include // ECDSA_SIG_*, i2d_ECDSA_SIG +#include // EVP_* +#include // OSSL_PARAM_* +#include // RSA_PKCS1_PSS_PADDING, RSA_PSS_SALTLEN_DIGEST + +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { + +// The parsed key keeps the native handle alive so that many signatures verify +// without rebuilding it +struct PublicKey::Internal { + PublicKey::Type kind; + EVP_PKEY *key; + // The stripped modulus, kept for the RSA signature range check + std::string modulus; + // The field width for the elliptic curve signature size check + std::size_t field_bytes; + // The expected signature length for the Edwards curve + std::size_t signature_bytes; +}; + +} // namespace sourcemeta::core + +namespace { + +auto to_message_digest( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> const EVP_MD * { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return EVP_sha256(); + case sourcemeta::core::SignatureHashFunction::SHA384: + return EVP_sha384(); + case sourcemeta::core::SignatureHashFunction::SHA512: + return EVP_sha512(); + } + + std::unreachable(); +} + +auto to_group_name(const sourcemeta::core::EllipticCurve curve) noexcept + -> const char * { + switch (curve) { + case sourcemeta::core::EllipticCurve::P256: + return "P-256"; + case sourcemeta::core::EllipticCurve::P384: + return "P-384"; + case sourcemeta::core::EllipticCurve::P521: + return "P-521"; + } + + std::unreachable(); +} + +auto to_pkey_id(const sourcemeta::core::EdwardsCurve curve) noexcept -> int { + switch (curve) { + case sourcemeta::core::EdwardsCurve::Ed25519: + return EVP_PKEY_ED25519; + case sourcemeta::core::EdwardsCurve::Ed448: + return EVP_PKEY_ED448; + } + + std::unreachable(); +} + +auto native_rsa_key(const std::string_view modulus, + const std::string_view exponent) -> EVP_PKEY * { + EVP_PKEY *result{nullptr}; + auto *modulus_number{ + BN_bin2bn(reinterpret_cast(modulus.data()), + static_cast(modulus.size()), nullptr)}; + auto *exponent_number{ + BN_bin2bn(reinterpret_cast(exponent.data()), + static_cast(exponent.size()), nullptr)}; + auto *builder{OSSL_PARAM_BLD_new()}; + + if (modulus_number != nullptr && exponent_number != nullptr && + builder != nullptr && + OSSL_PARAM_BLD_push_BN(builder, OSSL_PKEY_PARAM_RSA_N, modulus_number) == + 1 && + OSSL_PARAM_BLD_push_BN(builder, OSSL_PKEY_PARAM_RSA_E, exponent_number) == + 1) { + auto *parameters{OSSL_PARAM_BLD_to_param(builder)}; + if (parameters != nullptr) { + auto *context{EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr)}; + if (context != nullptr) { + if (EVP_PKEY_fromdata_init(context) == 1) { + EVP_PKEY_fromdata(context, &result, EVP_PKEY_PUBLIC_KEY, parameters); + } + + EVP_PKEY_CTX_free(context); + } + + OSSL_PARAM_free(parameters); + } + } + + OSSL_PARAM_BLD_free(builder); + BN_free(exponent_number); + BN_free(modulus_number); + return result; +} + +auto native_ec_key(const sourcemeta::core::EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) -> EVP_PKEY * { + const auto width{sourcemeta::core::curve_field_bytes(curve)}; + const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; + const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > width || stripped_y.size() > width) { + return nullptr; + } + + std::string point; + point.push_back('\x04'); + point.append(sourcemeta::core::pad_left(stripped_x, width, '\x00')); + point.append(sourcemeta::core::pad_left(stripped_y, width, '\x00')); + + EVP_PKEY *result{nullptr}; + auto *builder{OSSL_PARAM_BLD_new()}; + if (builder != nullptr && + OSSL_PARAM_BLD_push_utf8_string(builder, OSSL_PKEY_PARAM_GROUP_NAME, + to_group_name(curve), 0) == 1 && + OSSL_PARAM_BLD_push_octet_string( + builder, OSSL_PKEY_PARAM_PUB_KEY, + reinterpret_cast(point.data()), + point.size()) == 1) { + auto *parameters{OSSL_PARAM_BLD_to_param(builder)}; + if (parameters != nullptr) { + auto *context{EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr)}; + if (context != nullptr) { + if (EVP_PKEY_fromdata_init(context) == 1) { + EVP_PKEY_fromdata(context, &result, EVP_PKEY_PUBLIC_KEY, parameters); + } + + EVP_PKEY_CTX_free(context); + } + + OSSL_PARAM_free(parameters); + } + } + + OSSL_PARAM_BLD_free(builder); + return result; +} + +// Convert the raw fixed-width R || S concatenation into the DER signature that +// the verification interface expects +auto encode_ecdsa_signature(const std::string_view raw_signature, + unsigned char **output) -> int { + const auto half{raw_signature.size() / 2}; + auto *signature{ECDSA_SIG_new()}; + if (signature == nullptr) { + return -1; + } + + auto *r{ + BN_bin2bn(reinterpret_cast(raw_signature.data()), + static_cast(half), nullptr)}; + auto *s{BN_bin2bn( + reinterpret_cast(raw_signature.data() + half), + static_cast(half), nullptr)}; + if (r == nullptr || s == nullptr || ECDSA_SIG_set0(signature, r, s) != 1) { + BN_free(r); + BN_free(s); + ECDSA_SIG_free(signature); + return -1; + } + + const auto length{i2d_ECDSA_SIG(signature, output)}; + ECDSA_SIG_free(signature); + return length; +} + +auto verify_rsa(EVP_PKEY *key, + const sourcemeta::core::SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature, const bool probabilistic) + -> bool { + auto result{false}; + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + EVP_PKEY_CTX *key_context{nullptr}; + auto ready{EVP_DigestVerifyInit(context, &key_context, + to_message_digest(hash), nullptr, + key) == 1}; + if (ready && probabilistic) { + ready = EVP_PKEY_CTX_set_rsa_padding(key_context, + RSA_PKCS1_PSS_PADDING) == 1 && + EVP_PKEY_CTX_set_rsa_pss_saltlen(key_context, + RSA_PSS_SALTLEN_DIGEST) == 1; + } + + if (ready) { + result = EVP_DigestVerify( + context, + reinterpret_cast(signature.data()), + signature.size(), + reinterpret_cast(message.data()), + message.size()) == 1; + } + + EVP_MD_CTX_free(context); + } + + return result; +} + +} // namespace + +namespace sourcemeta::core { + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { + if (internal_ != nullptr) { + EVP_PKEY_free(internal_->key); + delete internal_; + } +} + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + if (internal_ != nullptr) { + EVP_PKEY_free(internal_->key); + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { return internal_->kind; } + +auto make_rsa_public_key(const std::string_view modulus, + const std::string_view exponent) + -> std::optional { + auto stripped_modulus{std::string{strip_left(modulus, '\x00')}}; + const auto stripped_exponent{strip_left(exponent, '\x00')}; + if (stripped_modulus.empty() || stripped_exponent.empty() || + stripped_modulus.size() > MAXIMUM_KEY_BYTES || + stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + return std::nullopt; + } + + auto *key{native_rsa_key(stripped_modulus, stripped_exponent)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .key = key, + .modulus = std::move(stripped_modulus), + .field_bytes = 0, + .signature_bytes = 0}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + auto *key{native_ec_key(curve, coordinate_x, coordinate_y)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .key = key, + .modulus = {}, + .field_bytes = curve_field_bytes(curve), + .signature_bytes = 0}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + auto *key{EVP_PKEY_new_raw_public_key( + to_pkey_id(curve), nullptr, + reinterpret_cast(public_key.data()), + public_key.size())}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .key = key, + .modulus = {}, + .field_bytes = 0, + .signature_bytes = eddsa_signature_bytes(curve)}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, false); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, true); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::EllipticCurve || + signature.size() != internal->field_bytes * 2) { + return false; + } + + unsigned char *der_signature{nullptr}; + const auto der_length{encode_ecdsa_signature(signature, &der_signature)}; + auto result{false}; + if (der_length > 0) { + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + if (EVP_DigestVerifyInit(context, nullptr, to_message_digest(hash), + nullptr, internal->key) == 1) { + result = + EVP_DigestVerify( + context, der_signature, static_cast(der_length), + reinterpret_cast(message.data()), + message.size()) == 1; + } + + EVP_MD_CTX_free(context); + } + } + + OPENSSL_free(der_signature); + return result; +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards || + signature.size() != internal->signature_bytes) { + return false; + } + + auto result{false}; + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + // EdDSA is a one-shot verification with a null digest, since the curve + // fixes the hash function internally + if (EVP_DigestVerifyInit(context, nullptr, nullptr, nullptr, + internal->key) == 1) { + result = EVP_DigestVerify( + context, + reinterpret_cast(signature.data()), + signature.size(), + reinterpret_cast(message.data()), + message.size()) == 1; + } + + EVP_MD_CTX_free(context); + } + + return result; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_other.cc b/vendor/core/src/core/crypto/crypto_verify_other.cc new file mode 100644 index 00000000..c441bab4 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_other.cc @@ -0,0 +1,478 @@ +#include +#include + +#include "crypto_bignum.h" +#include "crypto_ecc.h" +#include "crypto_eddsa.h" +#include "crypto_helpers.h" + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint32_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::unreachable + +namespace sourcemeta::core { +namespace { + +// The DigestInfo prefixes for the EMSA-PKCS1-v1_5 encoding, taken verbatim +// from RFC 8017 Section 9.2 Note 1 +constexpr std::array DIGEST_INFO_SHA256{ + {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}}; +constexpr std::array DIGEST_INFO_SHA384{ + {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30}}; +constexpr std::array DIGEST_INFO_SHA512{ + {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40}}; + +auto digest_info_prefix(const SignatureHashFunction hash) -> std::string_view { + switch (hash) { + case SignatureHashFunction::SHA256: + return {reinterpret_cast(DIGEST_INFO_SHA256.data()), + DIGEST_INFO_SHA256.size()}; + case SignatureHashFunction::SHA384: + return {reinterpret_cast(DIGEST_INFO_SHA384.data()), + DIGEST_INFO_SHA384.size()}; + case SignatureHashFunction::SHA512: + return {reinterpret_cast(DIGEST_INFO_SHA512.data()), + DIGEST_INFO_SHA512.size()}; + } + + std::unreachable(); +} + +// EMSA-PKCS1-v1_5 encoding (RFC 8017 Section 9.2) +auto build_encoded_message(const SignatureHashFunction hash, + const std::string_view message, + const std::size_t key_length) + -> std::optional { + const auto prefix{digest_info_prefix(hash)}; + const auto digest{digest_message(hash, message)}; + const auto encoded_length{prefix.size() + digest.size()}; + + // RFC 8017 Section 9.2 step 3: "If emLen < tLen + 11, output 'intended + // encoded message length too short'" + if (key_length < encoded_length + 11) { + return std::nullopt; + } + + std::string result; + result.reserve(key_length); + result.push_back('\x00'); + result.push_back('\x01'); + result.append(key_length - encoded_length - 3, '\xff'); + result.push_back('\x00'); + result.append(prefix); + result.append(digest); + return result; +} + +// MGF1 mask generation (RFC 8017 Appendix B.2.1) +auto mask_generation(const SignatureHashFunction hash, + const std::string_view seed, const std::size_t length) + -> std::string { + std::string result; + result.reserve(length + 64); + std::uint32_t counter{0}; + while (result.size() < length) { + std::string block{seed}; + block.push_back(static_cast((counter >> 24u) & 0xffu)); + block.push_back(static_cast((counter >> 16u) & 0xffu)); + block.push_back(static_cast((counter >> 8u) & 0xffu)); + block.push_back(static_cast(counter & 0xffu)); + result.append(digest_message(hash, block)); + counter += 1; + } + + result.resize(length); + return result; +} + +// EMSA-PSS verification (RFC 8017 Section 9.1.2), with the salt length fixed to +// the hash function output as RFC 7518 Section 3.5 requires +auto emsa_pss_verify(const SignatureHashFunction hash, + const std::string_view message, + const std::string_view encoded_message, + const std::size_t encoded_bits) -> bool { + const auto digest{digest_message(hash, message)}; + const auto hash_length{digest.size()}; + const auto salt_length{hash_length}; + const auto encoded_length{encoded_message.size()}; + + // RFC 8017 Section 9.1.2 step 3: "If emLen < hLen + sLen + 2, output + // 'inconsistent'" + if (encoded_length < hash_length + salt_length + 2) { + return false; + } + + // RFC 8017 Section 9.1.2 step 4: "If the rightmost octet of EM does not have + // hexadecimal value 0xbc, output 'inconsistent'" + if (static_cast(encoded_message.back()) != 0xbc) { + return false; + } + + const auto database_length{encoded_length - hash_length - 1}; + const auto masked_database{encoded_message.substr(0, database_length)}; + const auto hash_value{encoded_message.substr(database_length, hash_length)}; + + // RFC 8017 Section 9.1.2 step 6: "If the leftmost 8emLen - emBits bits of the + // leftmost octet in maskedDB are not all equal to zero, output + // 'inconsistent'" + const auto unused_bits{(8 * encoded_length) - encoded_bits}; + const auto unused_mask{ + static_cast((0xff00u >> unused_bits) & 0xffu)}; + if ((static_cast(masked_database.front()) & unused_mask) != 0) { + return false; + } + + auto database{mask_generation(hash, hash_value, database_length)}; + for (std::size_t index = 0; index < database_length; ++index) { + database[index] = + static_cast(database[index] ^ masked_database[index]); + } + + database[0] = static_cast(static_cast(database[0]) & + static_cast(~unused_mask)); + + // RFC 8017 Section 9.1.2 step 10: "If the emLen - hLen - sLen - 2 leftmost + // octets of DB are not zero or if the octet at position emLen - hLen - sLen - + // 1 does not have hexadecimal value 0x01, output 'inconsistent'" + const auto padding_length{encoded_length - hash_length - salt_length - 2}; + for (std::size_t index = 0; index < padding_length; ++index) { + if (database[index] != '\x00') { + return false; + } + } + + if (static_cast(database[padding_length]) != 0x01) { + return false; + } + + // RFC 8017 Section 9.1.2 steps 12 and 13: hash the concatenation of eight + // zero octets, the message digest, and the recovered salt + std::string verification_input(8, '\x00'); + verification_input.append(digest); + verification_input.append(database.substr(database_length - salt_length)); + const auto expected{digest_message(hash, verification_input)}; + return expected == hash_value; +} + +auto to_curve_parameters(const EllipticCurve curve) -> EllipticCurveParameters { + switch (curve) { + case EllipticCurve::P256: + return curve_p256(); + case EllipticCurve::P384: + return curve_p384(); + case EllipticCurve::P521: + return curve_p521(); + } + + std::unreachable(); +} + +// FIPS 186-4 Section 6.4 step 2, deriving the integer e from the leftmost bits +// of the message digest, truncated to the bit length of the order +auto digest_to_integer(const SignatureHashFunction hash, + const std::string_view message, + const std::size_t order_bits) -> Bignum { + const auto digest{digest_message(hash, message)}; + auto value{bignum_from_bytes(digest)}; + const auto digest_bits{digest.size() * 8}; + if (digest_bits > order_bits) { + value = bignum_shift_right(value, digest_bits - order_bits); + } + + return value; +} + +// RSASSA-PKCS1-v1_5 verification (RFC 8017 Section 8.2.2) over raw key material +auto verify_pkcs1(const SignatureHashFunction hash, + const std::string_view modulus, + const std::string_view exponent, + const std::string_view message, + const std::string_view signature) -> bool { + // RFC 8017 Section 8.2.2 step 1: "If the length of S is not k octets, output + // 'invalid signature'" + const auto key_length{modulus.size()}; + if (signature.size() != key_length) { + return false; + } + + const auto modulus_number{bignum_from_bytes(modulus)}; + const auto signature_number{bignum_from_bytes(signature)}; + + // RFC 8017 Section 5.2.2: "If the signature representative s is not between 0 + // and n - 1, output 'signature representative out of range'" + if (bignum_compare(signature_number, modulus_number) >= 0) { + return false; + } + + const auto exponent_number{bignum_from_bytes(exponent)}; + const auto message_representative{ + bignum_mod_exp(signature_number, exponent_number, modulus_number)}; + const auto encoded_message{ + bignum_to_bytes(message_representative, key_length)}; + const auto expected{build_encoded_message(hash, message, key_length)}; + return expected.has_value() && encoded_message == expected.value(); +} + +// RSASSA-PSS verification (RFC 8017 Section 8.1.2) over raw key material +auto verify_pss(const SignatureHashFunction hash, + const std::string_view modulus, const std::string_view exponent, + const std::string_view message, + const std::string_view signature) -> bool { + // RFC 8017 Section 8.1.2 step 1: "If the length of the signature S is not k + // octets, output 'invalid signature'" + const auto key_length{modulus.size()}; + if (signature.size() != key_length) { + return false; + } + + const auto modulus_number{bignum_from_bytes(modulus)}; + const auto signature_number{bignum_from_bytes(signature)}; + + // RFC 8017 Section 5.2.2: "If the signature representative s is not between 0 + // and n - 1, output 'signature representative out of range'" + if (bignum_compare(signature_number, modulus_number) >= 0) { + return false; + } + + const auto exponent_number{bignum_from_bytes(exponent)}; + const auto message_representative{ + bignum_mod_exp(signature_number, exponent_number, modulus_number)}; + + // RFC 8017 Section 8.1.2 step 2c: the encoded message is emLen octets long, + // where emLen equals the byte length of emBits = modBits - 1 bits, which is + // one octet less than k when the modulus bit length is congruent to one + // modulo eight + const auto encoded_bits{bignum_bit_length(modulus_number) - 1}; + const auto encoded_length{(encoded_bits + 7) / 8}; + const auto full_representative{ + bignum_to_bytes(message_representative, key_length)}; + for (std::size_t index = 0; index < key_length - encoded_length; ++index) { + if (full_representative[index] != '\x00') { + return false; + } + } + + const auto encoded_message{std::string_view{full_representative}.substr( + key_length - encoded_length)}; + return emsa_pss_verify(hash, message, encoded_message, encoded_bits); +} + +// ECDSA verification (FIPS 186-4 Section 6.4) over the raw public point +auto verify_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, + const std::string_view coordinate_x, + const std::string_view coordinate_y, + const std::string_view message, + const std::string_view signature) -> bool { + const auto parameters{to_curve_parameters(curve)}; + const auto field_bytes{parameters.field_bytes}; + + // RFC 7518 Section 3.4: the signature is the fixed-width concatenation of the + // two integers, each as long as the curve field + if (signature.size() != field_bytes * 2) { + return false; + } + + const auto r{bignum_from_bytes(signature.substr(0, field_bytes))}; + const auto s{bignum_from_bytes(signature.substr(field_bytes))}; + + // FIPS 186-4 Section 6.4.2 step 1: both integers must lie in [1, n - 1] + if (bignum_is_zero(r) || bignum_compare(r, parameters.order) >= 0 || + bignum_is_zero(s) || bignum_compare(s, parameters.order) >= 0) { + return false; + } + + // Reject coordinates wider than the field, matching the platform backends and + // preventing an oversized input from being truncated into a valid key + const auto stripped_x{strip_left(coordinate_x, '\x00')}; + const auto stripped_y{strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > field_bytes || stripped_y.size() > field_bytes) { + return false; + } + + const auto public_x{bignum_from_bytes(stripped_x)}; + const auto public_y{bignum_from_bytes(stripped_y)}; + + // The public key must be a valid point: coordinates below the field prime and + // satisfying the curve equation + if (bignum_compare(public_x, parameters.prime) >= 0 || + bignum_compare(public_y, parameters.prime) >= 0 || + !point_on_curve(public_x, public_y, parameters)) { + return false; + } + + const auto order_bits{bignum_bit_length(parameters.order)}; + const auto digest_integer{digest_to_integer(hash, message, order_bits)}; + const auto s_inverse{bignum_mod_inverse(s, parameters.order)}; + const auto u1{ + bignum_mod_multiply(digest_integer, s_inverse, parameters.order)}; + const auto u2{bignum_mod_multiply(r, s_inverse, parameters.order)}; + + const JacobianPoint generator{.x = parameters.generator_x, + .y = parameters.generator_y, + .z = bignum_from_u64(1)}; + const JacobianPoint public_point{ + .x = public_x, .y = public_y, .z = bignum_from_u64(1)}; + const auto point{point_double_scalar_multiply(u1, generator, u2, public_point, + parameters)}; + + // FIPS 186-4 Section 6.4.2 step 6: reject when the combination is the point + // at infinity + if (point_is_infinity(point)) { + return false; + } + + // FIPS 186-4 Section 6.4.2 step 7: the signature is valid when the affine x + // coordinate, reduced modulo the order, equals r + auto candidate{point_affine_x(point, parameters)}; + bignum_reduce(candidate, parameters.order); + return bignum_compare(candidate, r) == 0; +} + +} // namespace + +// The reference backend parses the key material into big integers inside each +// verification, which is cheap next to the modular arithmetic, so the parsed +// key simply holds the raw material +struct PublicKey::Internal { + PublicKey::Type kind; + std::string modulus; + std::string exponent; + std::string coordinate_x; + std::string coordinate_y; + EllipticCurve elliptic_curve; + EdwardsCurve edwards_curve; +}; + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { delete internal_; } + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + delete internal_; + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { return internal_->kind; } + +auto make_rsa_public_key(const std::string_view modulus, + const std::string_view exponent) + -> std::optional { + const auto stripped_modulus{strip_left(modulus, '\x00')}; + const auto stripped_exponent{strip_left(exponent, '\x00')}; + if (stripped_modulus.empty() || stripped_exponent.empty() || + stripped_modulus.size() > MAXIMUM_KEY_BYTES || + stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .modulus = std::string{stripped_modulus}, + .exponent = std::string{stripped_exponent}, + .coordinate_x = {}, + .coordinate_y = {}, + .elliptic_curve = {}, + .edwards_curve = {}}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + const auto width{curve_field_bytes(curve)}; + const auto stripped_x{strip_left(coordinate_x, '\x00')}; + const auto stripped_y{strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > width || stripped_y.size() > width) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .modulus = {}, + .exponent = {}, + .coordinate_x = std::string{stripped_x}, + .coordinate_y = std::string{stripped_y}, + .elliptic_curve = curve, + .edwards_curve = {}}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .modulus = {}, + .exponent = {}, + .coordinate_x = std::string{public_key}, + .coordinate_y = {}, + .elliptic_curve = {}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + return internal != nullptr && internal->kind == PublicKey::Type::RSA && + verify_pkcs1(hash, internal->modulus, internal->exponent, message, + signature); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + return internal != nullptr && internal->kind == PublicKey::Type::RSA && + verify_pss(hash, internal->modulus, internal->exponent, message, + signature); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + return internal != nullptr && + internal->kind == PublicKey::Type::EllipticCurve && + verify_ecdsa(internal->elliptic_curve, hash, internal->coordinate_x, + internal->coordinate_y, message, signature); +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards) { + return false; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: + return edwards25519_verify(internal->coordinate_x, message, signature); + case EdwardsCurve::Ed448: + return edwards448_verify(internal->coordinate_x, message, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_rsa_apple.cc b/vendor/core/src/core/crypto/crypto_verify_rsa_apple.cc deleted file mode 100644 index 6b58acff..00000000 --- a/vendor/core/src/core/crypto/crypto_verify_rsa_apple.cc +++ /dev/null @@ -1,150 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#include // CF*, kCF* -#include // Sec*, kSec* - -#include // std::array -#include // std::string -#include // std::string_view -#include // std::unreachable - -namespace { - -auto to_sec_key_pkcs1_v15_algorithm( - const sourcemeta::core::SignatureHashFunction hash) noexcept - -> SecKeyAlgorithm { - switch (hash) { - case sourcemeta::core::SignatureHashFunction::SHA256: - return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256; - case sourcemeta::core::SignatureHashFunction::SHA384: - return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA384; - case sourcemeta::core::SignatureHashFunction::SHA512: - return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA512; - } - - std::unreachable(); -} - -// These algorithm variants fix the salt length to the hash function -// output, which is exactly what RFC 7518 Section 3.5 requires -auto to_sec_key_pss_algorithm( - const sourcemeta::core::SignatureHashFunction hash) noexcept - -> SecKeyAlgorithm { - switch (hash) { - case sourcemeta::core::SignatureHashFunction::SHA256: - return kSecKeyAlgorithmRSASignatureMessagePSSSHA256; - case sourcemeta::core::SignatureHashFunction::SHA384: - return kSecKeyAlgorithmRSASignatureMessagePSSSHA384; - case sourcemeta::core::SignatureHashFunction::SHA512: - return kSecKeyAlgorithmRSASignatureMessagePSSSHA512; - } - - std::unreachable(); -} - -auto make_data(const std::string_view value) -> CFDataRef { - return CFDataCreate(kCFAllocatorDefault, - reinterpret_cast(value.data()), - static_cast(value.size())); -} - -auto make_rsa_public_key(const std::string_view modulus, - const std::string_view exponent) -> SecKeyRef { - // The platform expects the PKCS#1 RSAPublicKey structure, a DER sequence - // of the modulus and exponent integers (RFC 8017 Appendix A.1.1) - std::string body; - sourcemeta::core::der_append_unsigned_integer(body, modulus); - sourcemeta::core::der_append_unsigned_integer(body, exponent); - std::string der; - der.push_back('\x30'); - sourcemeta::core::der_append_length(der, body.size()); - der.append(body); - - auto key_data{make_data(der)}; - if (key_data == nullptr) { - return nullptr; - } - - std::array attribute_keys{ - {kSecAttrKeyType, kSecAttrKeyClass}}; - std::array attribute_values{ - {kSecAttrKeyTypeRSA, kSecAttrKeyClassPublic}}; - auto attributes{CFDictionaryCreate( - kCFAllocatorDefault, attribute_keys.data(), attribute_values.data(), - static_cast(attribute_keys.size()), - &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)}; - if (attributes == nullptr) { - CFRelease(key_data); - return nullptr; - } - - auto key{SecKeyCreateWithData(key_data, attributes, nullptr)}; - CFRelease(attributes); - CFRelease(key_data); - return key; -} - -auto verify_rsa_signature(const SecKeyAlgorithm algorithm, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - const auto stripped_modulus{sourcemeta::core::strip_left(modulus, '\x00')}; - const auto stripped_exponent{sourcemeta::core::strip_left(exponent, '\x00')}; - if (stripped_modulus.empty() || stripped_exponent.empty() || - stripped_modulus.size() > sourcemeta::core::MAXIMUM_KEY_BYTES || - stripped_exponent.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { - return false; - } - - auto key{make_rsa_public_key(stripped_modulus, stripped_exponent)}; - if (key == nullptr) { - return false; - } - - auto message_data{make_data(message)}; - auto signature_data{make_data(signature)}; - auto result{false}; - if (message_data != nullptr && signature_data != nullptr) { - result = SecKeyVerifySignature(key, algorithm, message_data, signature_data, - nullptr) == true; - } - - if (signature_data != nullptr) { - CFRelease(signature_data); - } - - if (message_data != nullptr) { - CFRelease(message_data); - } - - CFRelease(key); - return result; -} - -} // namespace - -namespace sourcemeta::core { - -auto rsassa_pkcs1_v15_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(to_sec_key_pkcs1_v15_algorithm(hash), modulus, - exponent, message, signature); -} - -auto rsassa_pss_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(to_sec_key_pss_algorithm(hash), modulus, exponent, - message, signature); -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_rsa_openssl.cc b/vendor/core/src/core/crypto/crypto_verify_rsa_openssl.cc deleted file mode 100644 index 52e55511..00000000 --- a/vendor/core/src/core/crypto/crypto_verify_rsa_openssl.cc +++ /dev/null @@ -1,144 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#include // BN_* -#include // OSSL_PKEY_PARAM_* -#include // EVP_* -#include // OSSL_PARAM_* -#include // RSA_PKCS1_PSS_PADDING, RSA_PSS_SALTLEN_DIGEST - -#include // std::string_view -#include // std::unreachable - -namespace { - -auto to_message_digest( - const sourcemeta::core::SignatureHashFunction hash) noexcept - -> const EVP_MD * { - switch (hash) { - case sourcemeta::core::SignatureHashFunction::SHA256: - return EVP_sha256(); - case sourcemeta::core::SignatureHashFunction::SHA384: - return EVP_sha384(); - case sourcemeta::core::SignatureHashFunction::SHA512: - return EVP_sha512(); - } - - std::unreachable(); -} - -auto make_rsa_public_key(const std::string_view modulus, - const std::string_view exponent) -> EVP_PKEY * { - EVP_PKEY *result{nullptr}; - auto *modulus_number{ - BN_bin2bn(reinterpret_cast(modulus.data()), - static_cast(modulus.size()), nullptr)}; - auto *exponent_number{ - BN_bin2bn(reinterpret_cast(exponent.data()), - static_cast(exponent.size()), nullptr)}; - auto *builder{OSSL_PARAM_BLD_new()}; - - if (modulus_number != nullptr && exponent_number != nullptr && - builder != nullptr && - OSSL_PARAM_BLD_push_BN(builder, OSSL_PKEY_PARAM_RSA_N, modulus_number) == - 1 && - OSSL_PARAM_BLD_push_BN(builder, OSSL_PKEY_PARAM_RSA_E, exponent_number) == - 1) { - auto *parameters{OSSL_PARAM_BLD_to_param(builder)}; - if (parameters != nullptr) { - auto *context{EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr)}; - if (context != nullptr) { - if (EVP_PKEY_fromdata_init(context) == 1) { - EVP_PKEY_fromdata(context, &result, EVP_PKEY_PUBLIC_KEY, parameters); - } - - EVP_PKEY_CTX_free(context); - } - - OSSL_PARAM_free(parameters); - } - } - - OSSL_PARAM_BLD_free(builder); - BN_free(exponent_number); - BN_free(modulus_number); - return result; -} - -auto verify_rsa_signature(const sourcemeta::core::SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature, - const bool probabilistic) -> bool { - const auto stripped_modulus{sourcemeta::core::strip_left(modulus, '\x00')}; - const auto stripped_exponent{sourcemeta::core::strip_left(exponent, '\x00')}; - if (stripped_modulus.empty() || stripped_exponent.empty() || - stripped_modulus.size() > sourcemeta::core::MAXIMUM_KEY_BYTES || - stripped_exponent.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { - return false; - } - - auto *key{make_rsa_public_key(stripped_modulus, stripped_exponent)}; - if (key == nullptr) { - return false; - } - - auto result{false}; - auto *context{EVP_MD_CTX_new()}; - if (context != nullptr) { - EVP_PKEY_CTX *key_context{nullptr}; - auto ready{EVP_DigestVerifyInit(context, &key_context, - to_message_digest(hash), nullptr, - key) == 1}; - if (ready && probabilistic) { - // Requesting the digest-length salt that RFC 7518 Section 3.5 - // requires makes verification reject signatures carrying any other - // salt length - ready = EVP_PKEY_CTX_set_rsa_padding(key_context, - RSA_PKCS1_PSS_PADDING) == 1 && - EVP_PKEY_CTX_set_rsa_pss_saltlen(key_context, - RSA_PSS_SALTLEN_DIGEST) == 1; - } - - if (ready) { - result = EVP_DigestVerify( - context, - reinterpret_cast(signature.data()), - signature.size(), - reinterpret_cast(message.data()), - message.size()) == 1; - } - - EVP_MD_CTX_free(context); - } - - EVP_PKEY_free(key); - return result; -} - -} // namespace - -namespace sourcemeta::core { - -auto rsassa_pkcs1_v15_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(hash, modulus, exponent, message, signature, - false); -} - -auto rsassa_pss_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(hash, modulus, exponent, message, signature, - true); -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_rsa_other.cc b/vendor/core/src/core/crypto/crypto_verify_rsa_other.cc deleted file mode 100644 index e70682ec..00000000 --- a/vendor/core/src/core/crypto/crypto_verify_rsa_other.cc +++ /dev/null @@ -1,258 +0,0 @@ -#include -#include - -#include "crypto_bignum.h" -#include "crypto_helpers.h" - -#include // std::array -#include // std::size_t -#include // std::uint8_t, std::uint32_t -#include // std::optional, std::nullopt -#include // std::string -#include // std::string_view -#include // std::unreachable - -namespace { - -// The DigestInfo prefixes for the EMSA-PKCS1-v1_5 encoding, taken verbatim -// from RFC 8017 Section 9.2 Note 1 -constexpr std::array DIGEST_INFO_SHA256{ - {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, - 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}}; -constexpr std::array DIGEST_INFO_SHA384{ - {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, - 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30}}; -constexpr std::array DIGEST_INFO_SHA512{ - {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, - 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40}}; - -auto digest_info_prefix(const sourcemeta::core::SignatureHashFunction hash) - -> std::string_view { - switch (hash) { - case sourcemeta::core::SignatureHashFunction::SHA256: - return {reinterpret_cast(DIGEST_INFO_SHA256.data()), - DIGEST_INFO_SHA256.size()}; - case sourcemeta::core::SignatureHashFunction::SHA384: - return {reinterpret_cast(DIGEST_INFO_SHA384.data()), - DIGEST_INFO_SHA384.size()}; - case sourcemeta::core::SignatureHashFunction::SHA512: - return {reinterpret_cast(DIGEST_INFO_SHA512.data()), - DIGEST_INFO_SHA512.size()}; - } - - std::unreachable(); -} - -// EMSA-PKCS1-v1_5 encoding (RFC 8017 Section 9.2) -auto build_encoded_message(const sourcemeta::core::SignatureHashFunction hash, - const std::string_view message, - const std::size_t key_length) - -> std::optional { - const auto prefix{digest_info_prefix(hash)}; - const auto digest{sourcemeta::core::digest_message(hash, message)}; - const auto encoded_length{prefix.size() + digest.size()}; - - // RFC 8017 Section 9.2 step 3: "If emLen < tLen + 11, output 'intended - // encoded message length too short'" - if (key_length < encoded_length + 11) { - return std::nullopt; - } - - std::string result; - result.reserve(key_length); - result.push_back('\x00'); - result.push_back('\x01'); - result.append(key_length - encoded_length - 3, '\xff'); - result.push_back('\x00'); - result.append(prefix); - result.append(digest); - return result; -} - -// MGF1 mask generation (RFC 8017 Appendix B.2.1) -auto mask_generation(const sourcemeta::core::SignatureHashFunction hash, - const std::string_view seed, const std::size_t length) - -> std::string { - std::string result; - result.reserve(length + 64); - std::uint32_t counter{0}; - while (result.size() < length) { - std::string block{seed}; - block.push_back(static_cast((counter >> 24u) & 0xffu)); - block.push_back(static_cast((counter >> 16u) & 0xffu)); - block.push_back(static_cast((counter >> 8u) & 0xffu)); - block.push_back(static_cast(counter & 0xffu)); - result.append(sourcemeta::core::digest_message(hash, block)); - counter += 1; - } - - result.resize(length); - return result; -} - -// EMSA-PSS verification (RFC 8017 Section 9.1.2), with the salt length -// fixed to the hash function output as RFC 7518 Section 3.5 requires -auto emsa_pss_verify(const sourcemeta::core::SignatureHashFunction hash, - const std::string_view message, - const std::string_view encoded_message, - const std::size_t encoded_bits) -> bool { - const auto digest{sourcemeta::core::digest_message(hash, message)}; - const auto hash_length{digest.size()}; - const auto salt_length{hash_length}; - const auto encoded_length{encoded_message.size()}; - - // RFC 8017 Section 9.1.2 step 3: "If emLen < hLen + sLen + 2, output - // 'inconsistent'" - if (encoded_length < hash_length + salt_length + 2) { - return false; - } - - // RFC 8017 Section 9.1.2 step 4: "If the rightmost octet of EM does not - // have hexadecimal value 0xbc, output 'inconsistent'" - if (static_cast(encoded_message.back()) != 0xbc) { - return false; - } - - const auto database_length{encoded_length - hash_length - 1}; - const auto masked_database{encoded_message.substr(0, database_length)}; - const auto hash_value{encoded_message.substr(database_length, hash_length)}; - - // RFC 8017 Section 9.1.2 step 6: "If the leftmost 8emLen - emBits bits of - // the leftmost octet in maskedDB are not all equal to zero, output - // 'inconsistent'" - const auto unused_bits{(8 * encoded_length) - encoded_bits}; - const auto unused_mask{ - static_cast((0xff00u >> unused_bits) & 0xffu)}; - if ((static_cast(masked_database.front()) & unused_mask) != 0) { - return false; - } - - auto database{mask_generation(hash, hash_value, database_length)}; - for (std::size_t index = 0; index < database_length; ++index) { - database[index] = - static_cast(database[index] ^ masked_database[index]); - } - - database[0] = static_cast(static_cast(database[0]) & - static_cast(~unused_mask)); - - // RFC 8017 Section 9.1.2 step 10: "If the emLen - hLen - sLen - 2 - // leftmost octets of DB are not zero or if the octet at position - // emLen - hLen - sLen - 1 does not have hexadecimal value 0x01, output - // 'inconsistent'" - const auto padding_length{encoded_length - hash_length - salt_length - 2}; - for (std::size_t index = 0; index < padding_length; ++index) { - if (database[index] != '\x00') { - return false; - } - } - - if (static_cast(database[padding_length]) != 0x01) { - return false; - } - - // RFC 8017 Section 9.1.2 steps 12 and 13: hash the concatenation of eight - // zero octets, the message digest, and the recovered salt - std::string verification_input(8, '\x00'); - verification_input.append(digest); - verification_input.append(database.substr(database_length - salt_length)); - const auto expected{ - sourcemeta::core::digest_message(hash, verification_input)}; - return expected == hash_value; -} - -} // namespace - -namespace sourcemeta::core { - -auto rsassa_pkcs1_v15_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - const auto stripped_modulus{sourcemeta::core::strip_left(modulus, '\x00')}; - const auto stripped_exponent{sourcemeta::core::strip_left(exponent, '\x00')}; - if (stripped_modulus.empty() || stripped_exponent.empty() || - stripped_modulus.size() > sourcemeta::core::MAXIMUM_KEY_BYTES || - stripped_exponent.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { - return false; - } - - // RFC 8017 Section 8.2.2 step 1: "If the length of S is not k octets, - // output 'invalid signature'" - const auto key_length{stripped_modulus.size()}; - if (signature.size() != key_length) { - return false; - } - - const auto modulus_number{bignum_from_bytes(stripped_modulus)}; - const auto signature_number{bignum_from_bytes(signature)}; - - // RFC 8017 Section 5.2.2: "If the signature representative s is not - // between 0 and n - 1, output 'signature representative out of range'" - if (bignum_compare(signature_number, modulus_number) >= 0) { - return false; - } - - const auto exponent_number{bignum_from_bytes(stripped_exponent)}; - const auto message_representative{ - bignum_mod_exp(signature_number, exponent_number, modulus_number)}; - const auto encoded_message{ - bignum_to_bytes(message_representative, key_length)}; - const auto expected{build_encoded_message(hash, message, key_length)}; - return expected.has_value() && encoded_message == expected.value(); -} - -auto rsassa_pss_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - const auto stripped_modulus{sourcemeta::core::strip_left(modulus, '\x00')}; - const auto stripped_exponent{sourcemeta::core::strip_left(exponent, '\x00')}; - if (stripped_modulus.empty() || stripped_exponent.empty() || - stripped_modulus.size() > sourcemeta::core::MAXIMUM_KEY_BYTES || - stripped_exponent.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { - return false; - } - - // RFC 8017 Section 8.1.2 step 1: "If the length of the signature S is not - // k octets, output 'invalid signature'" - const auto key_length{stripped_modulus.size()}; - if (signature.size() != key_length) { - return false; - } - - const auto modulus_number{bignum_from_bytes(stripped_modulus)}; - const auto signature_number{bignum_from_bytes(signature)}; - - // RFC 8017 Section 5.2.2: "If the signature representative s is not - // between 0 and n - 1, output 'signature representative out of range'" - if (bignum_compare(signature_number, modulus_number) >= 0) { - return false; - } - - const auto exponent_number{bignum_from_bytes(stripped_exponent)}; - const auto message_representative{ - bignum_mod_exp(signature_number, exponent_number, modulus_number)}; - - // RFC 8017 Section 8.1.2 step 2c: the encoded message is emLen octets - // long, where emLen equals the byte length of emBits = modBits - 1 bits, - // which is one octet less than k when the modulus bit length is congruent - // to one modulo eight - const auto encoded_bits{bignum_bit_length(modulus_number) - 1}; - const auto encoded_length{(encoded_bits + 7) / 8}; - const auto full_representative{ - bignum_to_bytes(message_representative, key_length)}; - for (std::size_t index = 0; index < key_length - encoded_length; ++index) { - if (full_representative[index] != '\x00') { - return false; - } - } - - const auto encoded_message{std::string_view{full_representative}.substr( - key_length - encoded_length)}; - return emsa_pss_verify(hash, message, encoded_message, encoded_bits); -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_rsa_windows.cc b/vendor/core/src/core/crypto/crypto_verify_rsa_windows.cc deleted file mode 100644 index 8b637964..00000000 --- a/vendor/core/src/core/crypto/crypto_verify_rsa_windows.cc +++ /dev/null @@ -1,137 +0,0 @@ -#include -#include - -#include "crypto_helpers.h" - -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include // ULONG, LPCWSTR - -#include // BCrypt*, BCRYPT_* - -#include // std::countl_zero -#include // std::size_t -#include // std::uint8_t -#include // std::memcpy -#include // std::string -#include // std::string_view -#include // std::unreachable - -namespace { - -auto to_cng_algorithm( - const sourcemeta::core::SignatureHashFunction hash) noexcept -> LPCWSTR { - switch (hash) { - case sourcemeta::core::SignatureHashFunction::SHA256: - return BCRYPT_SHA256_ALGORITHM; - case sourcemeta::core::SignatureHashFunction::SHA384: - return BCRYPT_SHA384_ALGORITHM; - case sourcemeta::core::SignatureHashFunction::SHA512: - return BCRYPT_SHA512_ALGORITHM; - } - - std::unreachable(); -} - -auto verify_rsa_signature(const sourcemeta::core::SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature, - const bool probabilistic) -> bool { - const auto stripped_modulus{sourcemeta::core::strip_left(modulus, '\x00')}; - const auto stripped_exponent{sourcemeta::core::strip_left(exponent, '\x00')}; - if (stripped_modulus.empty() || stripped_exponent.empty() || - stripped_modulus.size() > sourcemeta::core::MAXIMUM_KEY_BYTES || - stripped_exponent.size() > sourcemeta::core::MAXIMUM_KEY_BYTES) { - return false; - } - - const auto modulus_bit_length{ - (stripped_modulus.size() * 8u) - - static_cast(std::countl_zero( - static_cast(stripped_modulus.front())))}; - - BCRYPT_RSAKEY_BLOB header{}; - header.Magic = BCRYPT_RSAPUBLIC_MAGIC; - header.BitLength = static_cast(modulus_bit_length); - header.cbPublicExp = static_cast(stripped_exponent.size()); - header.cbModulus = static_cast(stripped_modulus.size()); - header.cbPrime1 = 0; - header.cbPrime2 = 0; - - std::string blob; - blob.resize(sizeof(header)); - std::memcpy(blob.data(), &header, sizeof(header)); - blob.append(stripped_exponent); - blob.append(stripped_modulus); - - BCRYPT_ALG_HANDLE algorithm{nullptr}; - if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( - &algorithm, BCRYPT_RSA_ALGORITHM, nullptr, 0))) { - return false; - } - - BCRYPT_KEY_HANDLE key{nullptr}; - if (!BCRYPT_SUCCESS( - BCryptImportKeyPair(algorithm, nullptr, BCRYPT_RSAPUBLIC_BLOB, &key, - reinterpret_cast(blob.data()), - static_cast(blob.size()), 0))) { - BCryptCloseAlgorithmProvider(algorithm, 0); - return false; - } - - const auto digest{sourcemeta::core::digest_message(hash, message)}; - - // The signature parameter is not const-qualified but is input only - auto result{false}; - if (probabilistic) { - // The digest-length salt is what RFC 7518 Section 3.5 requires - BCRYPT_PSS_PADDING_INFO padding{}; - padding.pszAlgId = to_cng_algorithm(hash); - padding.cbSalt = static_cast(digest.size()); - result = BCRYPT_SUCCESS(BCryptVerifySignature( - key, &padding, - reinterpret_cast(const_cast(digest.data())), - static_cast(digest.size()), - reinterpret_cast(const_cast(signature.data())), - static_cast(signature.size()), BCRYPT_PAD_PSS)); - } else { - BCRYPT_PKCS1_PADDING_INFO padding{}; - padding.pszAlgId = to_cng_algorithm(hash); - result = BCRYPT_SUCCESS(BCryptVerifySignature( - key, &padding, - reinterpret_cast(const_cast(digest.data())), - static_cast(digest.size()), - reinterpret_cast(const_cast(signature.data())), - static_cast(signature.size()), BCRYPT_PAD_PKCS1)); - } - - BCryptDestroyKey(key); - BCryptCloseAlgorithmProvider(algorithm, 0); - return result; -} - -} // namespace - -namespace sourcemeta::core { - -auto rsassa_pkcs1_v15_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(hash, modulus, exponent, message, signature, - false); -} - -auto rsassa_pss_verify(const SignatureHashFunction hash, - const std::string_view modulus, - const std::string_view exponent, - const std::string_view message, - const std::string_view signature) -> bool { - return verify_rsa_signature(hash, modulus, exponent, message, signature, - true); -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_windows.cc b/vendor/core/src/core/crypto/crypto_verify_windows.cc new file mode 100644 index 00000000..0e41c9e1 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_windows.cc @@ -0,0 +1,367 @@ +#include +#include + +#include "crypto_eddsa.h" +#include "crypto_helpers.h" + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG, LPCWSTR + +#include // BCrypt*, BCRYPT_* + +#include // std::countl_zero +#include // std::size_t +#include // std::uint8_t +#include // std::memcpy +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { + +// The parsed key keeps both the algorithm provider and the imported key handle +// alive for reuse. The Edwards curves have no CNG primitive, so they keep the +// raw encoded point and verify through the reference implementation +struct PublicKey::Internal { + PublicKey::Type kind; + BCRYPT_ALG_HANDLE algorithm; + BCRYPT_KEY_HANDLE key; + std::size_t field_bytes; + std::string modulus; + std::string edwards_point; + EdwardsCurve edwards_curve; +}; + +} // namespace sourcemeta::core + +namespace { + +auto to_cng_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept -> LPCWSTR { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return BCRYPT_SHA256_ALGORITHM; + case sourcemeta::core::SignatureHashFunction::SHA384: + return BCRYPT_SHA384_ALGORITHM; + case sourcemeta::core::SignatureHashFunction::SHA512: + return BCRYPT_SHA512_ALGORITHM; + } + + std::unreachable(); +} + +auto to_ecdsa_algorithm(const sourcemeta::core::EllipticCurve curve) noexcept + -> LPCWSTR { + switch (curve) { + case sourcemeta::core::EllipticCurve::P256: + return BCRYPT_ECDSA_P256_ALGORITHM; + case sourcemeta::core::EllipticCurve::P384: + return BCRYPT_ECDSA_P384_ALGORITHM; + case sourcemeta::core::EllipticCurve::P521: + return BCRYPT_ECDSA_P521_ALGORITHM; + } + + std::unreachable(); +} + +auto to_ecc_public_magic(const sourcemeta::core::EllipticCurve curve) noexcept + -> ULONG { + switch (curve) { + case sourcemeta::core::EllipticCurve::P256: + return BCRYPT_ECDSA_PUBLIC_P256_MAGIC; + case sourcemeta::core::EllipticCurve::P384: + return BCRYPT_ECDSA_PUBLIC_P384_MAGIC; + case sourcemeta::core::EllipticCurve::P521: + return BCRYPT_ECDSA_PUBLIC_P521_MAGIC; + } + + std::unreachable(); +} + +struct KeyPair { + BCRYPT_ALG_HANDLE algorithm; + BCRYPT_KEY_HANDLE key; +}; + +auto native_rsa_key(const std::string_view modulus, + const std::string_view exponent) -> KeyPair { + const auto modulus_bit_length{ + (modulus.size() * 8u) - static_cast(std::countl_zero( + static_cast(modulus.front())))}; + + BCRYPT_RSAKEY_BLOB header{}; + header.Magic = BCRYPT_RSAPUBLIC_MAGIC; + header.BitLength = static_cast(modulus_bit_length); + header.cbPublicExp = static_cast(exponent.size()); + header.cbModulus = static_cast(modulus.size()); + header.cbPrime1 = 0; + header.cbPrime2 = 0; + + std::string blob; + blob.resize(sizeof(header)); + std::memcpy(blob.data(), &header, sizeof(header)); + blob.append(exponent); + blob.append(modulus); + + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, BCRYPT_RSA_ALGORITHM, nullptr, 0))) { + return {.algorithm = nullptr, .key = nullptr}; + } + + BCRYPT_KEY_HANDLE key{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptImportKeyPair(algorithm, nullptr, BCRYPT_RSAPUBLIC_BLOB, &key, + reinterpret_cast(blob.data()), + static_cast(blob.size()), 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + return {.algorithm = nullptr, .key = nullptr}; + } + + return {.algorithm = algorithm, .key = key}; +} + +auto native_ec_key(const sourcemeta::core::EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y, const std::size_t width) + -> KeyPair { + BCRYPT_ECCKEY_BLOB header{}; + header.dwMagic = to_ecc_public_magic(curve); + header.cbKey = static_cast(width); + + std::string blob; + blob.resize(sizeof(header)); + std::memcpy(blob.data(), &header, sizeof(header)); + blob.append(sourcemeta::core::pad_left(coordinate_x, width, '\x00')); + blob.append(sourcemeta::core::pad_left(coordinate_y, width, '\x00')); + + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, to_ecdsa_algorithm(curve), nullptr, 0))) { + return {.algorithm = nullptr, .key = nullptr}; + } + + BCRYPT_KEY_HANDLE key{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptImportKeyPair(algorithm, nullptr, BCRYPT_ECCPUBLIC_BLOB, &key, + reinterpret_cast(blob.data()), + static_cast(blob.size()), 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + return {.algorithm = nullptr, .key = nullptr}; + } + + return {.algorithm = algorithm, .key = key}; +} + +auto verify_rsa(BCRYPT_KEY_HANDLE key, + const sourcemeta::core::SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature, const bool probabilistic) + -> bool { + const auto digest{sourcemeta::core::digest_message(hash, message)}; + + if (probabilistic) { + // The digest-length salt is what RFC 7518 Section 3.5 requires + BCRYPT_PSS_PADDING_INFO padding{}; + padding.pszAlgId = to_cng_algorithm(hash); + padding.cbSalt = static_cast(digest.size()); + return BCRYPT_SUCCESS(BCryptVerifySignature( + key, &padding, + reinterpret_cast(const_cast(digest.data())), + static_cast(digest.size()), + reinterpret_cast(const_cast(signature.data())), + static_cast(signature.size()), BCRYPT_PAD_PSS)); + } + + BCRYPT_PKCS1_PADDING_INFO padding{}; + padding.pszAlgId = to_cng_algorithm(hash); + return BCRYPT_SUCCESS(BCryptVerifySignature( + key, &padding, + reinterpret_cast(const_cast(digest.data())), + static_cast(digest.size()), + reinterpret_cast(const_cast(signature.data())), + static_cast(signature.size()), BCRYPT_PAD_PKCS1)); +} + +} // namespace + +namespace sourcemeta::core { + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + BCryptDestroyKey(internal_->key); + } + + if (internal_->algorithm != nullptr) { + BCryptCloseAlgorithmProvider(internal_->algorithm, 0); + } + + delete internal_; + } +} + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + BCryptDestroyKey(internal_->key); + } + + if (internal_->algorithm != nullptr) { + BCryptCloseAlgorithmProvider(internal_->algorithm, 0); + } + + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { return internal_->kind; } + +auto make_rsa_public_key(const std::string_view modulus, + const std::string_view exponent) + -> std::optional { + auto stripped_modulus{std::string{strip_left(modulus, '\x00')}}; + const auto stripped_exponent{strip_left(exponent, '\x00')}; + if (stripped_modulus.empty() || stripped_exponent.empty() || + stripped_modulus.size() > MAXIMUM_KEY_BYTES || + stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + return std::nullopt; + } + + const auto pair{native_rsa_key(stripped_modulus, stripped_exponent)}; + if (pair.key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .algorithm = pair.algorithm, + .key = pair.key, + .field_bytes = 0, + .modulus = std::move(stripped_modulus), + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + const auto width{curve_field_bytes(curve)}; + const auto stripped_x{strip_left(coordinate_x, '\x00')}; + const auto stripped_y{strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > width || stripped_y.size() > width) { + return std::nullopt; + } + + const auto pair{native_ec_key(curve, stripped_x, stripped_y, width)}; + if (pair.key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .algorithm = pair.algorithm, + .key = pair.key, + .field_bytes = width, + .modulus = {}, + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .algorithm = nullptr, + .key = nullptr, + .field_bytes = 0, + .modulus = {}, + .edwards_point = std::string{public_key}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, false); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, true); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::EllipticCurve || + signature.size() != internal->field_bytes * 2) { + return false; + } + + const auto digest{digest_message(hash, message)}; + + // The CNG signature format is the raw fixed-width R || S concatenation, so + // the input passes through unchanged + return BCRYPT_SUCCESS(BCryptVerifySignature( + internal->key, nullptr, + reinterpret_cast(const_cast(digest.data())), + static_cast(digest.size()), + reinterpret_cast(const_cast(signature.data())), + static_cast(signature.size()), 0)); +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards) { + return false; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: + return edwards25519_verify(internal->edwards_point, message, signature); + case EdwardsCurve::Ed448: + return edwards448_verify(internal->edwards_point, message, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_verify.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_verify.h index 6a033bef..38cc7ae3 100644 --- a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_verify.h +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_verify.h @@ -6,6 +6,7 @@ #endif #include // std::uint8_t +#include // std::optional #include // std::string_view namespace sourcemeta::core { @@ -18,67 +19,156 @@ enum class SignatureHashFunction : std::uint8_t { SHA256, SHA384, SHA512 }; /// The NIST elliptic curves supported by signature verification. enum class EllipticCurve : std::uint8_t { P256, P384, P521 }; +/// @ingroup crypto +/// The Edwards curves supported by signature verification. +enum class EdwardsCurve : std::uint8_t { Ed25519, Ed448 }; + +/// @ingroup crypto +/// A parsed public key that holds the native key, so that the same key can +/// verify many signatures without paying the key construction cost on every +/// call. Build it once with one of the factory functions and pass it to the +/// matching verification function. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_rsa_public_key(modulus, exponent)}; +/// assert(key.has_value()); +/// assert(sourcemeta::core::rsassa_pkcs1_v15_verify( +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, message, +/// signature)); +/// ``` +class SOURCEMETA_CORE_CRYPTO_EXPORT PublicKey { +public: + /// The kind of key, which fixes the signature schemes it can verify. + enum class Type : std::uint8_t { RSA, EllipticCurve, Edwards }; + + ~PublicKey(); + PublicKey(PublicKey &&other) noexcept; + auto operator=(PublicKey &&other) noexcept -> PublicKey &; + PublicKey(const PublicKey &) = delete; + auto operator=(const PublicKey &) -> PublicKey & = delete; + + /// The kind of key this is. + [[nodiscard]] auto type() const noexcept -> Type; + + /// The backend specific parsed key state, defined by each backend + struct Internal; + /// Take ownership of a parsed key. Prefer the factory functions below + explicit PublicKey(Internal *internal) noexcept; + /// Access the parsed key, which the verification functions read. The type is + /// opaque, so there is nothing a caller can do with it + [[nodiscard]] auto internal() const noexcept -> const Internal * { + return this->internal_; + } + +private: + Internal *internal_; +}; + +/// @ingroup crypto +/// Parse an RSA public key from its raw big-endian modulus and exponent bytes, +/// returning no value when the material is malformed or beyond 4096 bits. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_rsa_public_key( + const std::string_view modulus, const std::string_view exponent) + -> std::optional; + +/// @ingroup crypto +/// Parse an elliptic curve public key from its raw big-endian point +/// coordinates, returning no value when the point is malformed. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_ec_public_key( + const EllipticCurve curve, const std::string_view coordinate_x, + const std::string_view coordinate_y) -> std::optional; + +/// @ingroup crypto +/// Parse an Edwards-curve public key from its raw encoded point, returning no +/// value when the key is malformed or the wrong length for the curve. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_eddsa_public_key( + const EdwardsCurve curve, const std::string_view public_key) + -> std::optional; + /// @ingroup crypto /// Verify an RSASSA-PKCS1-v1_5 signature (RFC 8017 Section 8.2.2) over a -/// message, given the public key as raw big-endian modulus and exponent -/// bytes. The signature is invalid rather than an error if any input is -/// malformed, including keys beyond 4096 bits. For example: +/// message with the given RSA key. The signature is invalid rather than an +/// error if it is malformed or the key is not an RSA key. For example: /// /// ```cpp /// #include /// #include /// +/// const auto key{sourcemeta::core::make_rsa_public_key(modulus, exponent)}; +/// assert(key.has_value()); /// assert(!sourcemeta::core::rsassa_pkcs1_v15_verify( -/// sourcemeta::core::SignatureHashFunction::SHA256, -/// "", "", "message", "signature")); +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, "message", +/// "signature")); /// ``` auto SOURCEMETA_CORE_CRYPTO_EXPORT rsassa_pkcs1_v15_verify( - const SignatureHashFunction hash, const std::string_view modulus, - const std::string_view exponent, const std::string_view message, - const std::string_view signature) -> bool; + const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, const std::string_view signature) -> bool; /// @ingroup crypto -/// Verify an RSASSA-PSS signature (RFC 8017 Section 8.1.2) over a message, -/// given the public key as raw big-endian modulus and exponent bytes. The -/// salt is expected to be as long as the hash function output, as RFC 7518 -/// requires, and signatures carrying any other salt length are invalid, as -/// are keys beyond 4096 bits. For example: +/// Verify an RSASSA-PSS signature (RFC 8017 Section 8.1.2) over a message with +/// the given RSA key. The salt is expected to be as long as the hash function +/// output, as RFC 7518 requires, and signatures carrying any other salt length +/// are invalid, as are signatures verified against a non-RSA key. For example: /// /// ```cpp /// #include /// #include /// +/// const auto key{sourcemeta::core::make_rsa_public_key(modulus, exponent)}; +/// assert(key.has_value()); /// assert(!sourcemeta::core::rsassa_pss_verify( -/// sourcemeta::core::SignatureHashFunction::SHA256, -/// "", "", "message", "signature")); +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, "message", +/// "signature")); /// ``` auto SOURCEMETA_CORE_CRYPTO_EXPORT rsassa_pss_verify( - const SignatureHashFunction hash, const std::string_view modulus, - const std::string_view exponent, const std::string_view message, - const std::string_view signature) -> bool; + const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, const std::string_view signature) -> bool; /// @ingroup crypto -/// Verify an ECDSA signature (FIPS 186-4 Section 6.4) over a message, given -/// the public key as raw big-endian point coordinates. The signature is the -/// raw concatenation of the two integers, each padded to the curve field -/// width, as JWS mandates (RFC 7518 Section 3.4). The signature is invalid -/// rather than an error if any input is malformed or the point is not on the -/// curve. For example: +/// Verify an ECDSA signature (FIPS 186-4 Section 6.4) over a message with the +/// given elliptic curve key. The signature is the raw concatenation of the two +/// integers, each padded to the curve field width, as JWS mandates (RFC 7518 +/// Section 3.4). The signature is invalid rather than an error if it is +/// malformed or the key is not an elliptic curve key. For example: /// /// ```cpp /// #include /// #include /// +/// const auto key{sourcemeta::core::make_ec_public_key( +/// sourcemeta::core::EllipticCurve::P256, x, y)}; +/// assert(key.has_value()); /// assert(!sourcemeta::core::ecdsa_verify( -/// sourcemeta::core::EllipticCurve::P256, -/// sourcemeta::core::SignatureHashFunction::SHA256, -/// "", "", "message", "signature")); +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, "message", +/// "signature")); /// ``` auto SOURCEMETA_CORE_CRYPTO_EXPORT ecdsa_verify( - const EllipticCurve curve, const SignatureHashFunction hash, - const std::string_view coordinate_x, const std::string_view coordinate_y, + const PublicKey &key, const SignatureHashFunction hash, const std::string_view message, const std::string_view signature) -> bool; +/// @ingroup crypto +/// Verify an EdDSA signature (RFC 8032) over a message with the given Edwards +/// curve key. There is no separate hash function, as the curve fixes it. The +/// signature is invalid rather than an error if it is malformed or the key is +/// not an Edwards curve key. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_eddsa_public_key( +/// sourcemeta::core::EdwardsCurve::Ed25519, public_key)}; +/// assert(key.has_value()); +/// assert(!sourcemeta::core::eddsa_verify(key.value(), "message", +/// "signature")); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool; + } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/http/CMakeLists.txt b/vendor/core/src/core/http/CMakeLists.txt index ddc5d7d1..e81a3b9c 100644 --- a/vendor/core/src/core/http/CMakeLists.txt +++ b/vendor/core/src/core/http/CMakeLists.txt @@ -1,8 +1,19 @@ +if(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_curl.cc) +elseif(APPLE) + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_darwin.mm) +elseif(WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "MSYS") + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_windows.cc) +else() + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_curl.cc) +endif() + sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME http - PRIVATE_HEADERS problem.h status.h method.h message.h error.h + PRIVATE_HEADERS problem.h status.h method.h message.h error.h system.h SOURCES helpers.h problem.cc match_accept.cc match_accept_language.cc negotiate_encoding.cc from_date.cc format_link.cc field_list.cc - accept_includes_all.cc content_type_matches.cc parse_bearer.cc) + accept_includes_all.cc content_type_matches.cc parse_bearer.cc + ${SOURCEMETA_CORE_HTTP_CLIENT_SOURCE}) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME http) @@ -11,3 +22,17 @@ endif() target_link_libraries(sourcemeta_core_http PUBLIC sourcemeta::core::json) target_link_libraries(sourcemeta_core_http PUBLIC sourcemeta::core::text) target_link_libraries(sourcemeta_core_http PRIVATE sourcemeta::core::time) + +if(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) + find_package(CURL REQUIRED) + target_compile_definitions(sourcemeta_core_http + PRIVATE SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) + target_link_libraries(sourcemeta_core_http PRIVATE CURL::libcurl) +elseif(APPLE) + target_link_libraries(sourcemeta_core_http PRIVATE "-framework Foundation") +elseif(WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "MSYS") + target_link_libraries(sourcemeta_core_http PRIVATE winhttp) + target_link_libraries(sourcemeta_core_http PRIVATE sourcemeta::core::unicode) +else() + target_link_libraries(sourcemeta_core_http PRIVATE ${CMAKE_DL_LIBS}) +endif() diff --git a/src/http/http_curl.cc b/vendor/core/src/core/http/client_curl.cc similarity index 66% rename from src/http/http_curl.cc rename to vendor/core/src/core/http/client_curl.cc index 93d3cd12..0b7c89d9 100644 --- a/src/http/http_curl.cc +++ b/vendor/core/src/core/http/client_curl.cc @@ -1,6 +1,6 @@ -#include "http.h" +#include -#ifdef JSONSCHEMA_USE_SYSTEM_CURL +#ifdef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL #include // curl_easy_*, curl_slist_*, curl_global_init, CURLOPT_* #endif @@ -10,7 +10,7 @@ #include // std::string #include // std::string_view -#ifndef JSONSCHEMA_USE_SYSTEM_CURL +#ifndef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL #include // dlopen, dlsym, dlerror, RTLD_NOW #include // std::array @@ -53,8 +53,11 @@ constexpr CURLcode CURLE_OK{0}; constexpr long CURL_GLOBAL_ALL{3}; // SSL(1<<0) | WIN32(1<<1) constexpr CURLoption CURLOPT_URL{10002}; // STRINGPOINT + 2 constexpr CURLoption CURLOPT_FOLLOWLOCATION{52}; // LONG + 52 +constexpr CURLoption CURLOPT_MAXREDIRS{68}; // LONG + 68 constexpr CURLoption CURLOPT_NOSIGNAL{99}; // LONG + 99 constexpr CURLoption CURLOPT_ACCEPT_ENCODING{10102}; // STRINGPOINT + 102 +constexpr CURLoption CURLOPT_TIMEOUT_MS{155}; // LONG + 155 +constexpr CURLoption CURLOPT_CONNECTTIMEOUT_MS{156}; // LONG + 156 constexpr CURLoption CURLOPT_WRITEFUNCTION{20011}; // FUNCTIONPOINT + 11 constexpr CURLoption CURLOPT_WRITEDATA{10001}; // CBPOINT + 1 constexpr CURLoption CURLOPT_HEADERFUNCTION{20079}; // FUNCTIONPOINT + 79 @@ -65,15 +68,20 @@ constexpr CURLoption CURLOPT_HTTPHEADER{10023}; // SLISTPOINT + 23 constexpr CURLoption CURLOPT_NOBODY{44}; // LONG + 44 constexpr CURLoption CURLOPT_CUSTOMREQUEST{10036}; // STRINGPOINT + 36 constexpr CURLINFO CURLINFO_RESPONSE_CODE{2097154}; // CURLINFO_LONG(0x200000)+2 +constexpr CURLINFO CURLINFO_EFFECTIVE_URL{ + 1048577}; // CURLINFO_STRING(0x100000)+1 #endif -namespace sourcemeta::jsonschema { +namespace { + +constexpr std::string_view HTTP_RESPONSE_TOO_LARGE_MESSAGE{ + "The response exceeds the maximum allowed size"}; -// The subset of the libcurl C API this CLI relies on, captured as function -// pointers so the request logic is shared between the link-time backend -// (JSONSCHEMA_USE_SYSTEM_CURL) and the default runtime-loaded (dlopen) -// backend. Member types are derived from the curl headers, so they stay in -// sync with the real prototypes +// The subset of the libcurl C API this backend relies on, captured as +// function pointers so the request logic is shared between the link-time +// backend (SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) and the default +// runtime-loaded (dlopen) backend. Member types are derived from the curl +// headers, so they stay in sync with the real prototypes struct CurlApi { decltype(&curl_global_init) global_init; decltype(&curl_easy_init) easy_init; @@ -86,12 +94,6 @@ struct CurlApi { decltype(&curl_slist_free_all) slist_free_all; }; -} // namespace sourcemeta::jsonschema - -namespace { - -using sourcemeta::jsonschema::CurlApi; - class CurlHandle { public: explicit CurlHandle(const CurlApi &api) @@ -107,7 +109,7 @@ class CurlHandle { CurlHandle(CurlHandle &&) = delete; auto operator=(CurlHandle &&) -> CurlHandle & = delete; - auto get() const -> CURL * { return this->handle_; } + [[nodiscard]] auto get() const -> CURL * { return this->handle_; } explicit operator bool() const { return this->handle_ != nullptr; } private: @@ -136,7 +138,7 @@ class CurlHeaderList { } } - auto get() const -> curl_slist * { return this->list_; } + [[nodiscard]] auto get() const -> curl_slist * { return this->list_; } private: const CurlApi &api_; @@ -152,16 +154,17 @@ struct BodyContext { auto body_callback(char *data, std::size_t size, std::size_t count, void *user_data) -> std::size_t { auto *context{static_cast(user_data)}; + const std::size_t chunk{size * count}; if (context->maximum_size.has_value() && - context->output->size() + (size * count) > - context->maximum_size.value()) { + (context->output->size() > context->maximum_size.value() || + chunk > context->maximum_size.value() - context->output->size())) { context->maximum_size_exceeded = true; // Returning a smaller count than given aborts the transfer return 0; } - context->output->append(data, size * count); - return size * count; + context->output->append(data, chunk); + return chunk; } auto header_callback(char *data, std::size_t size, std::size_t count, @@ -172,107 +175,11 @@ auto header_callback(char *data, std::size_t size, std::size_t count, return size * count; } -} // namespace - -namespace sourcemeta::jsonschema { - -auto perform_request(const CurlApi &api, const HTTPRequest &request) - -> HTTPResponse { - static const CURLcode global_initialization{api.global_init(CURL_GLOBAL_ALL)}; - if (global_initialization != CURLE_OK) { - throw sourcemeta::core::HTTPError{request.method, std::string{request.url}, - api.easy_strerror(global_initialization)}; - } - - const CurlHandle handle{api}; - if (!handle) { - throw sourcemeta::core::HTTPError{request.method, std::string{request.url}, - "Failed to initialise the HTTP client"}; - } - - HTTPResponse response; - const std::string url{request.url}; - api.easy_setopt(handle.get(), CURLOPT_URL, url.c_str()); - api.easy_setopt(handle.get(), CURLOPT_FOLLOWLOCATION, 1L); - api.easy_setopt(handle.get(), CURLOPT_NOSIGNAL, 1L); - // Advertise and transparently decode all supported content encodings, - // matching what the NSURLSession and WinHTTP backends do - api.easy_setopt(handle.get(), CURLOPT_ACCEPT_ENCODING, ""); - - std::string raw_headers; - BodyContext body_context{&response.body, request.maximum_response_size}; - api.easy_setopt(handle.get(), CURLOPT_WRITEFUNCTION, body_callback); - api.easy_setopt(handle.get(), CURLOPT_WRITEDATA, &body_context); - api.easy_setopt(handle.get(), CURLOPT_HEADERFUNCTION, header_callback); - api.easy_setopt(handle.get(), CURLOPT_HEADERDATA, &raw_headers); - - CurlHeaderList header_list{api}; - for (const auto &[name, value] : request.headers) { - std::string line{name}; - // The semicolon form is how cURL distinguishes a header with an - // empty value from a header to suppress - if (value.empty()) { - line += ";"; - } else { - line += ": "; - line += value; - } - - header_list.append(line); - } - - if (request.body.has_value()) { - std::string content_type_line{"Content-Type: "}; - content_type_line += request.body.value().content_type; - header_list.append(content_type_line); - api.easy_setopt(handle.get(), CURLOPT_POSTFIELDSIZE_LARGE, - static_cast(request.body.value().data.size())); - api.easy_setopt(handle.get(), CURLOPT_POSTFIELDS, - request.body.value().data.data()); - } - - if (header_list.get()) { - api.easy_setopt(handle.get(), CURLOPT_HTTPHEADER, header_list.get()); - } - - const std::string method{ - sourcemeta::core::http_method_string(request.method)}; - if (request.method == sourcemeta::core::HTTPMethod::HEAD) { - api.easy_setopt(handle.get(), CURLOPT_NOBODY, 1L); - } else if (request.method != sourcemeta::core::HTTPMethod::GET || - request.body.has_value()) { - api.easy_setopt(handle.get(), CURLOPT_CUSTOMREQUEST, method.c_str()); - } - - const auto code{api.easy_perform(handle.get())}; - if (code != CURLE_OK) { - if (body_context.maximum_size_exceeded) { - throw sourcemeta::core::HTTPError{ - request.method, std::string{request.url}, - std::string{HTTP_RESPONSE_TOO_LARGE_MESSAGE}}; - } - - throw sourcemeta::core::HTTPError{request.method, std::string{request.url}, - api.easy_strerror(code)}; - } - - long status_code{0}; - api.easy_getinfo(handle.get(), CURLINFO_RESPONSE_CODE, &status_code); - sourcemeta::core::http_parse_headers(raw_headers, response.headers); - response.status = sourcemeta::core::http_status_from_code( - static_cast(status_code)); - return response; -} - -} // namespace sourcemeta::jsonschema +#ifndef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL -#ifndef JSONSCHEMA_USE_SYSTEM_CURL +using sourcemeta::core::HTTPSystemBackendError; -namespace { - -using sourcemeta::jsonschema::HTTPDynamicBackendNotFound; - -constexpr std::string_view CURL_LIBRARY_ENV{"SOURCEMETA_JSONSCHEMA_CURL_SO"}; +constexpr std::string_view CURL_LIBRARY_ENV{"SOURCEMETA_CORE_CURL_SO"}; // Tried in order. Every entry carries the `.so.4` SONAME so we only ever // bind an ABI-compatible cURL (never the unversioned `libcurl.so` dev @@ -303,7 +210,7 @@ auto resolve_symbol(const ResolvedLibrary &library, const char *name) dlerror(); void *symbol{dlsym(library.handle, name)}; if (dlerror() != nullptr) { - throw HTTPDynamicBackendNotFound{ + throw HTTPSystemBackendError{ "The cURL library was loaded but does not provide the expected API", std::string{CURL_LIBRARY_ENV}, {library.path}}; @@ -324,7 +231,7 @@ auto open_library() -> ResolvedLibrary { return {handle, configured_path}; } - throw HTTPDynamicBackendNotFound{ + throw HTTPSystemBackendError{ "Could not load the cURL library from the configured path", std::string{CURL_LIBRARY_ENV}, {std::string{configured_path}}}; @@ -342,7 +249,7 @@ auto open_library() -> ResolvedLibrary { searched.emplace_back(candidate); } - throw HTTPDynamicBackendNotFound{ + throw HTTPSystemBackendError{ "Could not find the system cURL library (libcurl)", std::string{CURL_LIBRARY_ENV}, std::move(searched)}; } @@ -352,42 +259,152 @@ auto load_curl() -> const CurlApi & { // must remain valid for the lifetime of the process static const ResolvedLibrary library{open_library()}; static const CurlApi api{ - resolve_symbol(library, - "curl_global_init"), - resolve_symbol(library, "curl_easy_init"), - resolve_symbol(library, - "curl_easy_cleanup"), - resolve_symbol(library, - "curl_easy_setopt"), - resolve_symbol(library, - "curl_easy_perform"), - resolve_symbol(library, - "curl_easy_getinfo"), - resolve_symbol(library, - "curl_easy_strerror"), - resolve_symbol(library, - "curl_slist_append"), - resolve_symbol(library, - "curl_slist_free_all")}; + .global_init = resolve_symbol( + library, "curl_global_init"), + .easy_init = resolve_symbol( + library, "curl_easy_init"), + .easy_cleanup = resolve_symbol( + library, "curl_easy_cleanup"), + .easy_setopt = resolve_symbol( + library, "curl_easy_setopt"), + .easy_perform = resolve_symbol( + library, "curl_easy_perform"), + .easy_getinfo = resolve_symbol( + library, "curl_easy_getinfo"), + .easy_strerror = resolve_symbol( + library, "curl_easy_strerror"), + .slist_append = resolve_symbol( + library, "curl_slist_append"), + .slist_free_all = resolve_symbol( + library, "curl_slist_free_all")}; return api; } -} // namespace - #endif -namespace sourcemeta::jsonschema { - -auto http_request(const HTTPRequest &request) -> HTTPResponse { -#ifdef JSONSCHEMA_USE_SYSTEM_CURL - static const CurlApi api{ - &curl_global_init, &curl_easy_init, &curl_easy_cleanup, - &curl_easy_setopt, &curl_easy_perform, &curl_easy_getinfo, - &curl_easy_strerror, &curl_slist_append, &curl_slist_free_all}; - return perform_request(api, request); +auto acquire_api() -> const CurlApi & { +#ifdef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL + static const CurlApi api{.global_init = &curl_global_init, + .easy_init = &curl_easy_init, + .easy_cleanup = &curl_easy_cleanup, + .easy_setopt = &curl_easy_setopt, + .easy_perform = &curl_easy_perform, + .easy_getinfo = &curl_easy_getinfo, + .easy_strerror = &curl_easy_strerror, + .slist_append = &curl_slist_append, + .slist_free_all = &curl_slist_free_all}; + return api; #else - return perform_request(load_curl(), request); + return load_curl(); #endif } -} // namespace sourcemeta::jsonschema +} // namespace + +namespace sourcemeta::core { + +auto HTTPSystemRequest::send() const -> HTTPResponse { + const CurlApi &api{acquire_api()}; + + static const CURLcode global_initialization{api.global_init(CURL_GLOBAL_ALL)}; + if (global_initialization != CURLE_OK) { + throw HTTPError{this->method_, this->url_, + api.easy_strerror(global_initialization)}; + } + + const CurlHandle handle{api}; + if (!handle) { + throw HTTPError{this->method_, this->url_, + "Failed to initialise the HTTP client"}; + } + + HTTPResponse response; + api.easy_setopt(handle.get(), CURLOPT_URL, this->url_.c_str()); + api.easy_setopt(handle.get(), CURLOPT_FOLLOWLOCATION, + this->follow_redirects_ ? 1L : 0L); + if (this->follow_redirects_) { + api.easy_setopt(handle.get(), CURLOPT_MAXREDIRS, + static_cast(this->maximum_redirects_)); + } + + api.easy_setopt(handle.get(), CURLOPT_NOSIGNAL, 1L); + api.easy_setopt(handle.get(), CURLOPT_TIMEOUT_MS, + static_cast(this->timeout_.count())); + if (this->connect_timeout_.has_value()) { + api.easy_setopt(handle.get(), CURLOPT_CONNECTTIMEOUT_MS, + static_cast(this->connect_timeout_.value().count())); + } + + // Advertise and transparently decode all supported content encodings, + // matching what the NSURLSession and WinHTTP backends do + api.easy_setopt(handle.get(), CURLOPT_ACCEPT_ENCODING, ""); + + std::string raw_headers; + BodyContext body_context{.output = &response.body, + .maximum_size = this->maximum_response_size_}; + api.easy_setopt(handle.get(), CURLOPT_WRITEFUNCTION, body_callback); + api.easy_setopt(handle.get(), CURLOPT_WRITEDATA, &body_context); + api.easy_setopt(handle.get(), CURLOPT_HEADERFUNCTION, header_callback); + api.easy_setopt(handle.get(), CURLOPT_HEADERDATA, &raw_headers); + + CurlHeaderList header_list{api}; + for (const auto &[name, value] : this->headers_) { + std::string line{name}; + // The semicolon form is how cURL distinguishes a header with an + // empty value from a header to suppress + if (value.empty()) { + line += ";"; + } else { + line += ": "; + line += value; + } + + header_list.append(line); + } + + if (this->body_.has_value()) { + std::string content_type_line{"Content-Type: "}; + content_type_line += this->body_.value().content_type; + header_list.append(content_type_line); + api.easy_setopt(handle.get(), CURLOPT_POSTFIELDSIZE_LARGE, + static_cast(this->body_.value().data.size())); + api.easy_setopt(handle.get(), CURLOPT_POSTFIELDS, + this->body_.value().data.data()); + } + + if (header_list.get()) { + api.easy_setopt(handle.get(), CURLOPT_HTTPHEADER, header_list.get()); + } + + const std::string method{http_method_string(this->method_)}; + if (this->method_ == HTTPMethod::HEAD) { + api.easy_setopt(handle.get(), CURLOPT_NOBODY, 1L); + } else if (this->method_ != HTTPMethod::GET || this->body_.has_value()) { + api.easy_setopt(handle.get(), CURLOPT_CUSTOMREQUEST, method.c_str()); + } + + const auto code{api.easy_perform(handle.get())}; + if (code != CURLE_OK) { + if (body_context.maximum_size_exceeded) { + throw HTTPError{this->method_, this->url_, + std::string{HTTP_RESPONSE_TOO_LARGE_MESSAGE}}; + } + + throw HTTPError{this->method_, this->url_, api.easy_strerror(code)}; + } + + long status_code{0}; + api.easy_getinfo(handle.get(), CURLINFO_RESPONSE_CODE, &status_code); + char *effective_url{nullptr}; + api.easy_getinfo(handle.get(), CURLINFO_EFFECTIVE_URL, &effective_url); + if (effective_url != nullptr) { + response.url.assign(effective_url); + } + + http_parse_headers(raw_headers, response.headers); + response.status = + http_status_from_code(static_cast(status_code)); + return response; +} + +} // namespace sourcemeta::core diff --git a/src/http/http_darwin.mm b/vendor/core/src/core/http/client_darwin.mm similarity index 55% rename from src/http/http_darwin.mm rename to vendor/core/src/core/http/client_darwin.mm index 43d63949..84d4b731 100644 --- a/src/http/http_darwin.mm +++ b/vendor/core/src/core/http/client_darwin.mm @@ -1,10 +1,9 @@ -#include "http.h" +#include +#include // NSURL, NSMutableURLRequest, NSURLSession, NSHTTPURLResponse, dispatch_* #import -#include - #include // std::size_t #include // std::uint16_t #include // std::string @@ -13,6 +12,9 @@ namespace { +constexpr std::string_view HTTP_RESPONSE_TOO_LARGE_MESSAGE{ + "The response exceeds the maximum allowed size"}; + auto to_nsstring(const std::string_view input) -> NSString * { return [[NSString alloc] initWithBytes:input.data() length:input.size() @@ -24,25 +26,52 @@ auto to_nsstring(const std::string_view input) -> NSString * { // The delegate-based API streams the response body in chunks, allowing // the maximum response size to be enforced without first buffering the // entire response in memory -@interface SourcemetaJSONSchemaHTTPDelegate - : NSObject -@property(nonatomic, assign) sourcemeta::jsonschema::HTTPResponse *response; +@interface SourcemetaCoreHTTPDelegate : NSObject +@property(nonatomic, assign) sourcemeta::core::HTTPResponse *response; @property(nonatomic, assign) std::string *failure; @property(nonatomic, strong) dispatch_semaphore_t semaphore; @property(nonatomic, assign) BOOL hasMaximumResponseSize; @property(nonatomic, assign) std::size_t maximumResponseSize; +@property(nonatomic, assign) BOOL followRedirects; +@property(nonatomic, assign) std::size_t maximumRedirects; +@property(nonatomic, assign) std::size_t redirectCount; @end -@implementation SourcemetaJSONSchemaHTTPDelegate +@implementation SourcemetaCoreHTTPDelegate + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + willPerformHTTPRedirection:(NSHTTPURLResponse *)response + newRequest:(NSURLRequest *)request + completionHandler: + (void (^)(NSURLRequest *))completionHandler { + // Passing a nil request stops the redirection and delivers the redirect + // response itself as the final response + if (!self.followRedirects) { + completionHandler(nil); + return; + } + + self.redirectCount += 1; + if (self.redirectCount > self.maximumRedirects) { + self.failure->assign("The maximum number of redirects was exceeded"); + [task cancel]; + completionHandler(nil); + return; + } + + completionHandler(request); +} - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { auto *body{&self.response->body}; if (self.hasMaximumResponseSize && - body->size() + data.length > self.maximumResponseSize) { - self.failure->assign( - sourcemeta::jsonschema::HTTP_RESPONSE_TOO_LARGE_MESSAGE); + (body->size() > self.maximumResponseSize || + static_cast(data.length) > + self.maximumResponseSize - body->size())) { + self.failure->assign(HTTP_RESPONSE_TOO_LARGE_MESSAGE); [dataTask cancel]; return; } @@ -67,6 +96,10 @@ - (void)URLSession:(NSURLSession *)session const auto *http_response{(NSHTTPURLResponse *)task.response}; self.response->status = sourcemeta::core::http_status_from_code( static_cast(http_response.statusCode)); + if (http_response.URL != nil) { + self.response->url.assign([http_response.URL.absoluteString UTF8String]); + } + auto *headers{&self.response->headers}; [http_response.allHeaderFields enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSString *value, @@ -83,9 +116,9 @@ - (void)URLSession:(NSURLSession *)session @end -namespace sourcemeta::jsonschema { +namespace sourcemeta::core { -auto http_request(const HTTPRequest &request) -> HTTPResponse { +auto HTTPSystemRequest::send() const -> HTTPResponse { HTTPResponse response; // The delegate runs on a background queue, where throwing would // terminate the process, so failures are recorded here and thrown @@ -93,45 +126,57 @@ auto http_request(const HTTPRequest &request) -> HTTPResponse { std::string failure; @autoreleasepool { - NSURL *target{[NSURL URLWithString:to_nsstring(request.url)]}; + NSURL *target{[NSURL URLWithString:to_nsstring(this->url_)]}; if (target == nil) { failure = "Invalid URL"; } else { NSMutableURLRequest *url_request{ [NSMutableURLRequest requestWithURL:target]}; - url_request.HTTPMethod = - to_nsstring(sourcemeta::core::http_method_string(request.method)); - for (const auto &[name, value] : request.headers) { + url_request.HTTPMethod = to_nsstring(http_method_string(this->method_)); + for (const auto &[name, value] : this->headers_) { // Repeated headers are folded into a single comma-separated field // line, which is semantically equivalent per RFC 9110 [url_request addValue:to_nsstring(value) forHTTPHeaderField:to_nsstring(name)]; } - if (request.body.has_value()) { - [url_request setValue:to_nsstring(request.body.value().content_type) + if (this->body_.has_value()) { + [url_request setValue:to_nsstring(this->body_.value().content_type) forHTTPHeaderField:@"Content-Type"]; url_request.HTTPBody = - [NSData dataWithBytes:request.body.value().data.data() - length:request.body.value().data.size()]; + [NSData dataWithBytes:this->body_.value().data.data() + length:this->body_.value().data.size()]; + } + + NSURLSessionConfiguration *configuration{ + [NSURLSessionConfiguration ephemeralSessionConfiguration]}; + configuration.timeoutIntervalForResource = + static_cast(this->timeout_.count()) / 1000.0; + if (this->connect_timeout_.has_value()) { + configuration.timeoutIntervalForRequest = + static_cast(this->connect_timeout_.value().count()) / + 1000.0; } // The delegate completes before the semaphore is signalled, so // pointing to the stack-allocated locals from it is safe - SourcemetaJSONSchemaHTTPDelegate *delegate{ - [[SourcemetaJSONSchemaHTTPDelegate alloc] init]}; + SourcemetaCoreHTTPDelegate *delegate{ + [[SourcemetaCoreHTTPDelegate alloc] init]}; delegate.response = &response; delegate.failure = &failure; delegate.semaphore = dispatch_semaphore_create(0); delegate.hasMaximumResponseSize = - request.maximum_response_size.has_value() ? YES : NO; - delegate.maximumResponseSize = request.maximum_response_size.value_or(0); - - NSURLSession *session{[NSURLSession - sessionWithConfiguration:[NSURLSessionConfiguration - ephemeralSessionConfiguration] - delegate:delegate - delegateQueue:nil]}; + this->maximum_response_size_.has_value() ? YES : NO; + delegate.maximumResponseSize = + this->maximum_response_size_.value_or(0); + delegate.followRedirects = this->follow_redirects_ ? YES : NO; + delegate.maximumRedirects = this->maximum_redirects_; + delegate.redirectCount = 0; + + NSURLSession *session{ + [NSURLSession sessionWithConfiguration:configuration + delegate:delegate + delegateQueue:nil]}; NSURLSessionDataTask *task{[session dataTaskWithRequest:url_request]}; [task resume]; dispatch_semaphore_wait(delegate.semaphore, DISPATCH_TIME_FOREVER); @@ -140,11 +185,10 @@ auto http_request(const HTTPRequest &request) -> HTTPResponse { } if (!failure.empty()) { - throw sourcemeta::core::HTTPError{request.method, std::string{request.url}, - failure}; + throw HTTPError{this->method_, this->url_, failure}; } return response; } -} // namespace sourcemeta::jsonschema +} // namespace sourcemeta::core diff --git a/src/http/http_windows.cc b/vendor/core/src/core/http/client_windows.cc similarity index 53% rename from src/http/http_windows.cc rename to vendor/core/src/core/http/client_windows.cc index 586f6174..609115db 100644 --- a/src/http/http_windows.cc +++ b/vendor/core/src/core/http/client_windows.cc @@ -1,4 +1,4 @@ -#include "http.h" +#include #ifndef NOMINMAX #define NOMINMAX @@ -18,15 +18,36 @@ #include +#include // std::chrono::milliseconds +#include // std::size_t #include // std::uint16_t #include // std::numeric_limits -#include // std::string +#include // std::string, std::wstring #include // std::wstring_view #include // std::pair #include // std::vector namespace { +constexpr std::string_view HTTP_RESPONSE_TOO_LARGE_MESSAGE{ + "The response exceeds the maximum allowed size"}; + +// WinHttpSetTimeouts takes signed millisecond counts where zero requests no +// timeout. Floor non-positive durations to the smallest bound so a misused +// value cannot become an unbounded wait, and saturate large ones to avoid a +// narrowing wrap +auto to_winhttp_timeout(const std::chrono::milliseconds value) -> int { + if (value.count() <= 0) { + return 1; + } + + if (value.count() > std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + + return static_cast(value.count()); +} + class WinHTTPHandle { public: WinHTTPHandle(const HINTERNET handle) : handle_{handle} {} @@ -70,22 +91,41 @@ auto parse_response_headers( headers); } +auto query_effective_url(const HINTERNET request) -> std::string { + DWORD size{0}; + WinHttpQueryOption(request, WINHTTP_OPTION_URL, nullptr, &size); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || size == 0) { + return {}; + } + + std::wstring buffer(size / sizeof(wchar_t), L'\0'); + if (!WinHttpQueryOption(request, WINHTTP_OPTION_URL, buffer.data(), &size)) { + return {}; + } + + buffer.resize(size / sizeof(wchar_t)); + if (!buffer.empty() && buffer.back() == L'\0') { + buffer.pop_back(); + } + + return sourcemeta::core::wide_to_utf8(buffer); +} + } // namespace -namespace sourcemeta::jsonschema { +namespace sourcemeta::core { -auto http_request(const HTTPRequest &request) -> HTTPResponse { +auto HTTPSystemRequest::send() const -> HTTPResponse { HTTPResponse response; - const auto wide_url{sourcemeta::core::utf8_to_wide(request.url)}; + const auto wide_url{sourcemeta::core::utf8_to_wide(this->url_)}; URL_COMPONENTS components{}; components.dwStructSize = sizeof(components); components.dwHostNameLength = static_cast(-1); components.dwUrlPathLength = static_cast(-1); components.dwExtraInfoLength = static_cast(-1); if (!WinHttpCrackUrl(wide_url.c_str(), 0, 0, &components)) { - throw sourcemeta::core::HTTPError{request.method, std::string{request.url}, - "Invalid URL"}; + throw HTTPError{this->method_, this->url_, "Invalid URL"}; } const std::wstring host{components.lpszHostName, components.dwHostNameLength}; @@ -101,49 +141,68 @@ auto http_request(const HTTPRequest &request) -> HTTPResponse { WinHttpOpen(nullptr, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0)}; if (!session) { - throw sourcemeta::core::HTTPError{request.method, std::string{request.url}, - "Failed to initialise the HTTP client"}; + throw HTTPError{this->method_, this->url_, + "Failed to initialise the HTTP client"}; } const WinHTTPHandle connection{ WinHttpConnect(session.get(), host.c_str(), components.nPort, 0)}; if (!connection) { - throw sourcemeta::core::HTTPError{request.method, std::string{request.url}, - "Failed to connect to the host"}; + throw HTTPError{this->method_, this->url_, "Failed to connect to the host"}; } const auto secure{components.nScheme == INTERNET_SCHEME_HTTPS}; - const auto method{sourcemeta::core::utf8_to_wide( - sourcemeta::core::http_method_string(request.method))}; + const auto method{ + sourcemeta::core::utf8_to_wide(http_method_string(this->method_))}; const WinHTTPHandle request_handle{WinHttpOpenRequest( connection.get(), method.c_str(), path.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, secure ? WINHTTP_FLAG_SECURE : 0)}; if (!request_handle) { - throw sourcemeta::core::HTTPError{request.method, std::string{request.url}, - "Failed to create the HTTP request"}; + throw HTTPError{this->method_, this->url_, + "Failed to create the HTTP request"}; } + if (this->follow_redirects_) { + DWORD maximum_redirects{static_cast(this->maximum_redirects_)}; + WinHttpSetOption(request_handle.get(), + WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS, + &maximum_redirects, sizeof(maximum_redirects)); + } else { + DWORD policy{WINHTTP_OPTION_REDIRECT_POLICY_NEVER}; + WinHttpSetOption(request_handle.get(), WINHTTP_OPTION_REDIRECT_POLICY, + &policy, sizeof(policy)); + } + + // The total timeout bounds sending the request and receiving the response, + // and also caps the resolution and connection phases unless a narrower + // connect timeout is given, so it acts as an overall ceiling + const auto total_timeout{to_winhttp_timeout(this->timeout_)}; + const auto connect_timeout{ + this->connect_timeout_.has_value() + ? to_winhttp_timeout(this->connect_timeout_.value()) + : total_timeout}; + WinHttpSetTimeouts(request_handle.get(), connect_timeout, connect_timeout, + total_timeout, total_timeout); + DWORD decompression{WINHTTP_DECOMPRESSION_FLAG_ALL}; WinHttpSetOption(request_handle.get(), WINHTTP_OPTION_DECOMPRESSION, &decompression, sizeof(decompression)); - auto serialized_headers{ - sourcemeta::core::http_serialize_headers(request.headers)}; + auto serialized_headers{http_serialize_headers(this->headers_)}; LPVOID body_data{WINHTTP_NO_REQUEST_DATA}; DWORD body_size{0}; - if (request.body.has_value()) { - if (request.body.value().data.size() > std::numeric_limits::max()) { - throw sourcemeta::core::HTTPError{request.method, - std::string{request.url}, - "The request body is too large"}; + if (this->body_.has_value()) { + if (this->body_.value().data.size() > std::numeric_limits::max()) { + throw HTTPError{this->method_, this->url_, + "The request body is too large"}; } serialized_headers += "Content-Type: "; - serialized_headers += request.body.value().content_type; + serialized_headers += this->body_.value().content_type; serialized_headers += "\r\n"; - body_data = const_cast(request.body.value().data.data()); - body_size = static_cast(request.body.value().data.size()); + body_data = const_cast(this->body_.value().data.data()); + body_size = static_cast(this->body_.value().data.size()); } const auto request_headers{ @@ -156,13 +215,13 @@ auto http_request(const HTTPRequest &request) -> HTTPResponse { request_headers.empty() ? 0 : static_cast(request_headers.size()), body_data, body_size, body_size, 0)) { - throw sourcemeta::core::HTTPError{request.method, std::string{request.url}, - "Failed to send the HTTP request"}; + throw HTTPError{this->method_, this->url_, + "Failed to send the HTTP request"}; } if (!WinHttpReceiveResponse(request_handle.get(), nullptr)) { - throw sourcemeta::core::HTTPError{request.method, std::string{request.url}, - "Failed to receive the HTTP response"}; + throw HTTPError{this->method_, this->url_, + "Failed to receive the HTTP response"}; } DWORD status_code{0}; @@ -172,31 +231,30 @@ auto http_request(const HTTPRequest &request) -> HTTPResponse { WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &status_code_size, WINHTTP_NO_HEADER_INDEX)) { - throw sourcemeta::core::HTTPError{ - request.method, std::string{request.url}, - "Failed to read the HTTP response status"}; + throw HTTPError{this->method_, this->url_, + "Failed to read the HTTP response status"}; } parse_response_headers(request_handle.get(), response.headers); + response.url = query_effective_url(request_handle.get()); while (true) { DWORD available{0}; if (!WinHttpQueryDataAvailable(request_handle.get(), &available)) { - throw sourcemeta::core::HTTPError{ - request.method, std::string{request.url}, - "Failed to read the HTTP response body"}; + throw HTTPError{this->method_, this->url_, + "Failed to read the HTTP response body"}; } if (available == 0) { break; } - if (request.maximum_response_size.has_value() && - response.body.size() + available > - request.maximum_response_size.value()) { - throw sourcemeta::core::HTTPError{ - request.method, std::string{request.url}, - std::string{HTTP_RESPONSE_TOO_LARGE_MESSAGE}}; + if (this->maximum_response_size_.has_value() && + (response.body.size() > this->maximum_response_size_.value() || + available > + this->maximum_response_size_.value() - response.body.size())) { + throw HTTPError{this->method_, this->url_, + std::string{HTTP_RESPONSE_TOO_LARGE_MESSAGE}}; } const auto offset{response.body.size()}; @@ -204,17 +262,16 @@ auto http_request(const HTTPRequest &request) -> HTTPResponse { DWORD read{0}; if (!WinHttpReadData(request_handle.get(), response.body.data() + offset, available, &read)) { - throw sourcemeta::core::HTTPError{ - request.method, std::string{request.url}, - "Failed to read the HTTP response body"}; + throw HTTPError{this->method_, this->url_, + "Failed to read the HTTP response body"}; } response.body.resize(offset + read); } - response.status = sourcemeta::core::http_status_from_code( - static_cast(status_code)); + response.status = + http_status_from_code(static_cast(status_code)); return response; } -} // namespace sourcemeta::jsonschema +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http.h b/vendor/core/src/core/http/include/sourcemeta/core/http.h index 59028c61..f572a6e3 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http.h @@ -11,6 +11,7 @@ #include #include #include +#include // NOLINTEND(misc-include-cleaner) #include // std::chrono::system_clock diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_system.h b/vendor/core/src/core/http/include/sourcemeta/core/http_system.h new file mode 100644 index 00000000..2d85dcbf --- /dev/null +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_system.h @@ -0,0 +1,192 @@ +#ifndef SOURCEMETA_CORE_HTTP_SYSTEM_H_ +#define SOURCEMETA_CORE_HTTP_SYSTEM_H_ + +#ifndef SOURCEMETA_CORE_HTTP_EXPORT +#include +#endif + +#include +#include + +#include // std::chrono::milliseconds, std::chrono::seconds +#include // std::size_t +#include // std::optional +#include // std::runtime_error +#include // std::string +#include // std::move, std::pair +#include // std::vector + +namespace sourcemeta::core { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup http +/// The result of performing a request against a system HTTP backend. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// sourcemeta::core::HTTPSystemRequest request{"https://example.com"}; +/// const auto response{request.send()}; +/// assert(response.status == sourcemeta::core::HTTP_STATUS_OK); +/// ``` +struct HTTPResponse { + /// The response status code + HTTPStatus status{}; + /// The response headers, with names normalised to lowercase. Repeated + /// headers are preserved as separate entries, except on backends that fold + /// them into a single comma-separated entry, which is semantically + /// equivalent per RFC 9110 + std::vector> headers; + /// The response body, owned by this result + std::string body; + /// The effective URL after any followed redirects + std::string url; +}; + +/// @ingroup http +/// An error that prevented loading the underlying system HTTP backend, such +/// as a missing dynamically loaded library. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const sourcemeta::core::HTTPSystemBackendError error{ +/// "Could not find the system cURL library", "SOURCEMETA_CORE_CURL_SO", +/// {"libcurl.so.4"}}; +/// assert(error.variable() == "SOURCEMETA_CORE_CURL_SO"); +/// ``` +class SOURCEMETA_CORE_HTTP_EXPORT HTTPSystemBackendError + : public std::runtime_error { +public: + HTTPSystemBackendError(const std::string &message, std::string variable, + std::vector paths) + : std::runtime_error{message}, variable_{std::move(variable)}, + paths_{std::move(paths)} {} + + /// Get the name of the environment variable that overrides the backend path + [[nodiscard]] auto variable() const noexcept -> const std::string & { + return this->variable_; + } + + /// Get the paths that were searched while looking for the backend + [[nodiscard]] auto paths() const noexcept + -> const std::vector & { + return this->paths_; + } + +private: + std::string variable_; + std::vector paths_; +}; + +/// @ingroup http +/// A simple cross-platform HTTP request that delegates to the system HTTP +/// stack, NSURLSession on Apple platforms, WinHTTP on Windows, and cURL +/// everywhere else. The request owns its data, configure it with the builder +/// methods and perform it with `send`. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// sourcemeta::core::HTTPSystemRequest request{ +/// "https://example.com", sourcemeta::core::HTTPMethod::POST}; +/// request.header("Accept", "application/json"); +/// request.body("{}", "application/json"); +/// const auto response{request.send()}; +/// assert(response.status == sourcemeta::core::HTTP_STATUS_OK); +/// ``` +class SOURCEMETA_CORE_HTTP_EXPORT HTTPSystemRequest { +public: + explicit HTTPSystemRequest(std::string url, + const HTTPMethod method = HTTPMethod::GET) + : url_{std::move(url)}, method_{method} {} + + /// Set the request method + auto method(const HTTPMethod method) -> HTTPSystemRequest & { + this->method_ = method; + return *this; + } + + /// Add a request header. Repeated names are permitted + auto header(std::string name, std::string value) -> HTTPSystemRequest & { + this->headers_.emplace_back(std::move(name), std::move(value)); + return *this; + } + + /// Set the request body, sent along with the given `Content-Type` header + auto body(std::string data, std::string content_type) -> HTTPSystemRequest & { + this->body_ = + Body{.data = std::move(data), .content_type = std::move(content_type)}; + return *this; + } + + /// Set whether to follow redirects, on by default + auto follow_redirects(const bool value) -> HTTPSystemRequest & { + this->follow_redirects_ = value; + return *this; + } + + /// Set the maximum number of redirects to follow, 20 by default + auto maximum_redirects(const std::size_t value) -> HTTPSystemRequest & { + this->maximum_redirects_ = value; + return *this; + } + + /// Set the total request timeout, 30 seconds by default + auto timeout(const std::chrono::milliseconds value) -> HTTPSystemRequest & { + this->timeout_ = value; + return *this; + } + + /// Set a best-effort timeout for establishing the connection, applied as + /// each backend allows and falling back to the backend default when unset + auto connect_timeout(const std::chrono::milliseconds value) + -> HTTPSystemRequest & { + this->connect_timeout_ = value; + return *this; + } + + /// Abort with an error if the response body exceeds this number of bytes + auto maximum_response_size(const std::size_t value) -> HTTPSystemRequest & { + this->maximum_response_size_ = value; + return *this; + } + + /// Perform the request. A failure to obtain a response is reported as an + /// error, while unsuccessful status codes are returned on the result + [[nodiscard]] auto send() const -> HTTPResponse; + +private: + struct Body { + std::string data; + std::string content_type; + }; + + std::string url_; + HTTPMethod method_; + std::vector> headers_; + std::optional body_; + bool follow_redirects_{true}; + std::size_t maximum_redirects_{20}; + std::chrono::milliseconds timeout_{std::chrono::seconds{30}}; + std::optional connect_timeout_; + std::optional maximum_response_size_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jose/CMakeLists.txt b/vendor/core/src/core/jose/CMakeLists.txt index 61e8195c..3923db25 100644 --- a/vendor/core/src/core/jose/CMakeLists.txt +++ b/vendor/core/src/core/jose/CMakeLists.txt @@ -1,11 +1,17 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jose - PRIVATE_HEADERS algorithm.h error.h jwk.h jwks.h - SOURCES jose_algorithm.cc jose_jwk.cc jose_jwks.cc) + PRIVATE_HEADERS algorithm.h error.h jwk.h jwks.h jwt.h verify.h + SOURCES jose_algorithm.cc jose_jwk.cc jose_jwks.cc jose_jwt.cc + jose_jwt_check_claims.cc jose_jws_verify_signature.cc + jose_jwt_verify_signature.cc jose_jwt_verify.cc) target_link_libraries(sourcemeta_core_jose PUBLIC sourcemeta::core::json) target_link_libraries(sourcemeta_core_jose - PRIVATE sourcemeta::core::crypto) + PUBLIC sourcemeta::core::crypto) +target_link_libraries(sourcemeta_core_jose + PRIVATE sourcemeta::core::text) +target_link_libraries(sourcemeta_core_jose + PRIVATE sourcemeta::core::time) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME jose) diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose.h index c67ef0d8..47193a2a 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose.h @@ -10,6 +10,8 @@ #include #include #include +#include +#include // NOLINTEND(misc-include-cleaner) /// @defgroup jose JOSE diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h index 0172d01b..3f938c10 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h @@ -12,9 +12,10 @@ namespace sourcemeta::core { /// @ingroup jose -/// The asymmetric JSON Web Signature algorithms from RFC 7518 Section 3.1. The -/// symmetric HMAC family and the null algorithm are intentionally absent, which -/// makes algorithm confusion attacks unrepresentable in the type system. +/// The asymmetric JSON Web Signature algorithms from RFC 7518 Section 3.1 and +/// the Edwards-curve algorithm from RFC 8037 Section 3.1. The symmetric HMAC +/// family and the null algorithm are intentionally absent, which makes +/// algorithm confusion attacks unrepresentable in the type system. enum class JWSAlgorithm : std::uint8_t { RS256, RS384, @@ -24,7 +25,8 @@ enum class JWSAlgorithm : std::uint8_t { PS512, ES256, ES384, - ES512 + ES512, + EdDSA }; /// @ingroup jose diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_error.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_error.h index aa56fdef..9cd17fc5 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose_error.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_error.h @@ -31,6 +31,15 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWKSParseError : public std::exception { } }; +/// @ingroup jose +/// An error that occurs when parsing an invalid JSON Web Token. +class SOURCEMETA_CORE_JOSE_EXPORT JWTParseError : public std::exception { +public: + [[nodiscard]] auto what() const noexcept -> const char * override { + return "The input is not a valid JSON Web Token"; + } +}; + #if defined(_MSC_VER) #pragma warning(default : 4251 4275) #endif diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h index 3a315c35..879873b9 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h @@ -10,6 +10,7 @@ #include // NOLINTEND(misc-include-cleaner) +#include #include #include // std::uint8_t @@ -20,9 +21,9 @@ namespace sourcemeta::core { /// @ingroup jose -/// A parsed public JSON Web Key (RFC 7517), restricted to RSA and elliptic -/// curve keys. The key owns its decoded material, so the source JSON document -/// does not need to outlive it. For example: +/// A parsed public JSON Web Key (RFC 7517), restricted to RSA, elliptic curve, +/// and octet key pair (RFC 8037) keys. The key owns its decoded material, so +/// the source JSON document does not need to outlive it. For example: /// /// ```cpp /// #include @@ -37,7 +38,7 @@ namespace sourcemeta::core { /// ``` class SOURCEMETA_CORE_JOSE_EXPORT JWK { public: - enum class Type : std::uint8_t { RSA, EllipticCurve }; + enum class Type : std::uint8_t { RSA, EllipticCurve, OctetKeyPair }; /// Parse a JSON Web Key from a JSON value, throwing on invalid input. explicit JWK(const JSON &value); @@ -45,6 +46,12 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWK { /// Parse a JSON Web Key from a JSON value, throwing on invalid input. explicit JWK(JSON &&value); + /// A key exclusively owns its parsed public key, so it is move-only. + JWK(JWK &&other) noexcept = default; + auto operator=(JWK &&other) noexcept -> JWK & = default; + JWK(const JWK &) = delete; + auto operator=(const JWK &) -> JWK & = delete; + /// Parse a JSON Web Key from a JSON value, returning no value on invalid /// input. [[nodiscard]] static auto from(const JSON &value) -> std::optional; @@ -68,26 +75,18 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWK { return this->algorithm_; } - // RSA keys (RFC 7518 Section 6.3): big-endian modulus and exponent - [[nodiscard]] auto modulus() const noexcept -> std::string_view { - return this->modulus_; - } - - [[nodiscard]] auto exponent() const noexcept -> std::string_view { - return this->exponent_; - } - - // Elliptic curve keys (RFC 7518 Section 6.2): curve name and coordinates + // Elliptic curve keys (RFC 7518 Section 6.2) and octet key pairs (RFC 8037 + // Section 2) carry a curve name, which the elliptic curve algorithms pin to + // exactly one curve [[nodiscard]] auto curve() const noexcept -> std::string_view { return this->curve_; } - [[nodiscard]] auto coordinate_x() const noexcept -> std::string_view { - return this->coordinate_x_; - } - - [[nodiscard]] auto coordinate_y() const noexcept -> std::string_view { - return this->coordinate_y_; + // The parsed platform key, built once from the decoded material so that + // verification reuses it rather than reconstructing it per signature. It is + // null when the material could not be turned into a key + [[nodiscard]] auto public_key() const noexcept -> const PublicKey * { + return this->public_key_.has_value() ? &*this->public_key_ : nullptr; } private: @@ -100,11 +99,8 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWK { Type type_{Type::RSA}; std::optional key_id_; std::optional algorithm_; - std::string modulus_; - std::string exponent_; std::string curve_; - std::string coordinate_x_; - std::string coordinate_y_; + std::optional public_key_; #if defined(_MSC_VER) #pragma warning(default : 4251) #endif diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h index 8dc94084..9880b683 100644 --- a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h @@ -44,6 +44,12 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWKS { explicit JWKS(const JSON &value); explicit JWKS(JSON &&value); + /// A key set exclusively owns its keys, so it is move-only. + JWKS(JWKS &&other) noexcept = default; + auto operator=(JWKS &&other) noexcept -> JWKS & = default; + JWKS(const JWKS &) = delete; + auto operator=(const JWKS &) -> JWKS & = delete; + /// Parse a JSON Web Key Set from a JSON value, returning no value on invalid /// input. [[nodiscard]] static auto from(const JSON &value) -> std::optional; diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwt.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwt.h new file mode 100644 index 00000000..a4b51abc --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwt.h @@ -0,0 +1,118 @@ +#ifndef SOURCEMETA_CORE_JOSE_JWT_H_ +#define SOURCEMETA_CORE_JOSE_JWT_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include + +#include // std::chrono::system_clock::time_point +#include // std::optional +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup jose +/// A parsed JSON Web Token in compact serialization (RFC 7519, RFC 7515). The +/// token does not own its input, so the string it was parsed from must outlive +/// it. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY21lIn0.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// assert(token.value().algorithm() == sourcemeta::core::JWSAlgorithm::RS256); +/// ``` +class SOURCEMETA_CORE_JOSE_EXPORT JWT { +public: + /// Parse a JSON Web Token from its compact serialization, throwing a + /// `JWTParseError` on invalid input. + explicit JWT(const std::string_view input); + + /// Parse a JSON Web Token from its compact serialization, returning no value + /// on invalid input. + [[nodiscard]] static auto from(const std::string_view input) + -> std::optional; + + // Header (RFC 7515 Section 4) + + [[nodiscard]] auto algorithm() const noexcept -> std::optional { + return this->algorithm_; + } + + [[nodiscard]] auto key_id() const noexcept -> std::optional; + + [[nodiscard]] auto type() const noexcept -> std::optional; + + [[nodiscard]] auto header() const noexcept -> const JSON & { + return this->header_; + } + + // Registered claims (RFC 7519 Section 4.1) + + [[nodiscard]] auto issuer() const noexcept -> std::optional; + + [[nodiscard]] auto subject() const noexcept + -> std::optional; + + [[nodiscard]] auto + has_audience(const std::string_view audience) const noexcept -> bool; + + [[nodiscard]] auto expires_at() const + -> std::optional; + + [[nodiscard]] auto not_before() const + -> std::optional; + + [[nodiscard]] auto issued_at() const + -> std::optional; + + [[nodiscard]] auto token_id() const noexcept + -> std::optional; + + [[nodiscard]] auto payload() const noexcept -> const JSON & { + return this->payload_; + } + + // The exact wire bytes the signature is computed over, never re-serialized + // (RFC 7515 Section 5.1) + [[nodiscard]] auto signing_input() const noexcept -> std::string_view { + return this->signing_input_; + } + + [[nodiscard]] auto signature() const noexcept -> std::string_view { + return this->signature_; + } + +private: + JWT() = default; + static auto parse(const std::string_view input, JWT &result) -> bool; + +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + std::string_view signing_input_; + std::string signature_; + JSON header_{nullptr}; + JSON payload_{nullptr}; + std::optional algorithm_; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif +}; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h new file mode 100644 index 00000000..30b9d37d --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h @@ -0,0 +1,178 @@ +#ifndef SOURCEMETA_CORE_JOSE_VERIFY_H_ +#define SOURCEMETA_CORE_JOSE_VERIFY_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include // std::chrono::seconds, std::chrono::system_clock +#include // std::uint8_t +#include // std::optional +#include // std::span +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup jose +/// The claim validation errors that claim checking can return, one per check +/// performed rather than an exhaustive list of registered claims. +enum class JWTClaimError : std::uint8_t { + Issuer, + Subject, + Audience, + Expiration, + NotBefore, + IssuedAt +}; + +/// @ingroup jose +/// Validate the registered claims of a JSON Web Token against the expected +/// issuer and audience at a given time, returning the first failing check or no +/// value when every check passes. The expiration claim is required (RFC 9068 +/// Section 2.2), and the subject is checked only when an expected value is +/// supplied. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9." +/// "eyJpc3MiOiJhY21lIiwiYXVkIjoiY2xpZW50IiwiZXhwIjoyMDAwMDAwMDAwfQ.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// const auto error{sourcemeta::core::jwt_check_claims( +/// token.value(), "acme", "client", +/// std::chrono::system_clock::from_time_t(1500000000))}; +/// assert(!error.has_value()); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jwt_check_claims( + const JWT &token, const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew = std::chrono::seconds{0}, + const std::optional expected_subject = std::nullopt) + -> std::optional; + +/// @ingroup jose +/// Verify a JSON Web Signature given its algorithm, its signing input, and its +/// decoded signature against a JSON Web Key, returning false rather than +/// throwing for an unrecognized algorithm, a key whose type or curve cannot +/// serve the algorithm, a key declaring a contradicting algorithm, or a +/// signature that does not verify. The signing input is the exact bytes the +/// signature was computed over, which carry no constraint on their content. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::JWK::from(sourcemeta::core::parse_json( +/// R"JSON({ "kty": "RSA", "n": "", "e": "" })JSON"))}; +/// assert(!key.has_value() || +/// !sourcemeta::core::jws_verify_signature( +/// sourcemeta::core::JWSAlgorithm::RS256, "header.payload", +/// "signature", key.value())); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jws_verify_signature(const std::optional algorithm, + const std::string_view signing_input, + const std::string_view signature, const JWK &key) + -> bool; + +/// @ingroup jose +/// Verify the signature of a JSON Web Token against a JSON Web Key, returning +/// false rather than throwing whenever the token does not carry a confirmed +/// valid signature for the key. This includes an unrecognized algorithm, a key +/// whose type or curve cannot serve the algorithm, a key declaring a +/// contradicting algorithm, and a signature that does not verify. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY21lIn0.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// const auto key{sourcemeta::core::JWK::from( +/// sourcemeta::core::parse_json(R"JSON({ +/// "kty": "RSA", "n": "", "e": "" +/// })JSON"))}; +/// assert(!key.has_value() || +/// !sourcemeta::core::jwt_verify_signature(token.value(), key.value())); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jwt_verify_signature(const JWT &token, const JWK &key) -> bool; + +/// @ingroup jose +/// The steps of full token verification that can fail, in the order they are +/// evaluated. +enum class JWTVerificationError : std::uint8_t { + AlgorithmNotAllowed, + UnknownKey, + Signature, + Type, + Issuer, + Subject, + Audience, + Expiration, + NotBefore, + IssuedAt +}; + +/// @ingroup jose +/// Verify a JSON Web Token end to end against a key set, in the mandated order: +/// the algorithm must be in the allow-list, a key is selected by its identifier +/// or, when absent, tried against every compatible key, the signature must +/// verify, and the claims must pass. Returns no value when the token is fully +/// valid, or the first failing step. The type check enforces the access token +/// profile (RFC 9068 Section 2.1) only when an expected type is supplied. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY21lIn0.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// const auto keys{sourcemeta::core::JWKS::from( +/// sourcemeta::core::parse_json(R"JSON({ "keys": [] })JSON"))}; +/// assert(keys.has_value()); +/// const std::array allowed{sourcemeta::core::JWSAlgorithm::RS256}; +/// const auto error{sourcemeta::core::jwt_verify( +/// token.value(), keys.value(), allowed, "acme", "client", +/// std::chrono::system_clock::from_time_t(1500000000))}; +/// assert(error.has_value()); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jwt_verify( + const JWT &token, const JWKS &keys, + const std::span allowed_algorithms, + const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew = std::chrono::seconds{0}, + const std::optional expected_subject = std::nullopt, + const std::optional expected_type = std::nullopt) + -> std::optional; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jose/jose_algorithm.cc b/vendor/core/src/core/jose/jose_algorithm.cc index e7e39c89..185877eb 100644 --- a/vendor/core/src/core/jose/jose_algorithm.cc +++ b/vendor/core/src/core/jose/jose_algorithm.cc @@ -25,6 +25,8 @@ auto to_jws_algorithm(const std::string_view value) noexcept return JWSAlgorithm::ES384; } else if (value == "ES512") { return JWSAlgorithm::ES512; + } else if (value == "EdDSA") { + return JWSAlgorithm::EdDSA; } else { return std::nullopt; } diff --git a/vendor/core/src/core/jose/jose_jwk.cc b/vendor/core/src/core/jose/jose_jwk.cc index 4d581ee0..5e249603 100644 --- a/vendor/core/src/core/jose/jose_jwk.cc +++ b/vendor/core/src/core/jose/jose_jwk.cc @@ -25,8 +25,9 @@ const auto HASH_DQ{sourcemeta::core::JSON::Object::hash("dq")}; const auto HASH_QI{sourcemeta::core::JSON::Object::hash("qi")}; const auto HASH_OTH{sourcemeta::core::JSON::Object::hash("oth")}; -// The RSA algorithms only require an RSA key, while each ECDSA algorithm is -// tied to a specific curve (RFC 7518 Section 3.1) +// The RSA algorithms only require an RSA key, each ECDSA algorithm is tied to a +// specific curve (RFC 7518 Section 3.1), and the Edwards-curve algorithm +// requires an octet key pair of either curve (RFC 8037 Section 3.1) auto algorithm_matches_key(const sourcemeta::core::JWSAlgorithm algorithm, const sourcemeta::core::JWK::Type type, const std::string_view curve) -> bool { @@ -47,6 +48,8 @@ auto algorithm_matches_key(const sourcemeta::core::JWSAlgorithm algorithm, case sourcemeta::core::JWSAlgorithm::ES512: return type == sourcemeta::core::JWK::Type::EllipticCurve && curve == "P-521"; + case sourcemeta::core::JWSAlgorithm::EdDSA: + return type == sourcemeta::core::JWK::Type::OctetKeyPair; } std::unreachable(); @@ -66,6 +69,39 @@ auto ec_coordinate_bytes(const std::string_view curve) } } +// The public key octet length is fixed per Edwards curve (RFC 8032 Sections +// 5.1.5 and 5.2.5) +auto okp_key_bytes(const std::string_view curve) -> std::optional { + if (curve == "Ed25519") { + return 32; + } else if (curve == "Ed448") { + return 57; + } else { + return std::nullopt; + } +} + +// Both mappings are only reached after the curve has been validated above +auto to_elliptic_curve(const std::string_view curve) noexcept + -> sourcemeta::core::EllipticCurve { + if (curve == "P-256") { + return sourcemeta::core::EllipticCurve::P256; + } else if (curve == "P-384") { + return sourcemeta::core::EllipticCurve::P384; + } else { + return sourcemeta::core::EllipticCurve::P521; + } +} + +auto to_edwards_curve(const std::string_view curve) noexcept + -> sourcemeta::core::EdwardsCurve { + if (curve == "Ed25519") { + return sourcemeta::core::EdwardsCurve::Ed25519; + } else { + return sourcemeta::core::EdwardsCurve::Ed448; + } +} + } // namespace namespace sourcemeta::core { @@ -81,6 +117,7 @@ auto JWK::parse(const JSON &value, JWK &result) -> bool { } const auto &key_type_value{key_type->to_string()}; + std::optional parsed_key; if (key_type_value == "RSA") { // A public key must not carry the private parameters (RFC 7518 Section // 6.3.2), and rejecting them early surfaces dangerous misconfigurations @@ -109,8 +146,8 @@ auto JWK::parse(const JSON &value, JWK &result) -> bool { } result.type_ = Type::RSA; - result.modulus_ = std::move(decoded_modulus).value(); - result.exponent_ = std::move(decoded_exponent).value(); + parsed_key = + make_rsa_public_key(decoded_modulus.value(), decoded_exponent.value()); } else if (key_type_value == "EC") { // A public key must not carry the private parameter (RFC 7518 Section // 6.2.2) @@ -143,8 +180,36 @@ auto JWK::parse(const JSON &value, JWK &result) -> bool { result.type_ = Type::EllipticCurve; result.curve_ = curve->to_string(); - result.coordinate_x_ = std::move(decoded_x).value(); - result.coordinate_y_ = std::move(decoded_y).value(); + parsed_key = make_ec_public_key(to_elliptic_curve(result.curve_), + decoded_x.value(), decoded_y.value()); + } else if (key_type_value == "OKP") { + // A public key must not carry the private parameter (RFC 8037 Section 2) + if (value.try_at("d", HASH_D) != nullptr) { + return false; + } + + const auto *curve{value.try_at("crv", HASH_CRV)}; + const auto *public_key{value.try_at("x", HASH_X)}; + if (curve == nullptr || !curve->is_string() || public_key == nullptr || + !public_key->is_string()) { + return false; + } + + const auto key_bytes{okp_key_bytes(curve->to_string())}; + if (!key_bytes.has_value()) { + return false; + } + + auto decoded_public_key{base64url_decode(public_key->to_string())}; + if (!decoded_public_key.has_value() || + decoded_public_key.value().size() != key_bytes.value()) { + return false; + } + + result.type_ = Type::OctetKeyPair; + result.curve_ = curve->to_string(); + parsed_key = make_eddsa_public_key(to_edwards_curve(result.curve_), + decoded_public_key.value()); } else { return false; } @@ -174,6 +239,10 @@ auto JWK::parse(const JSON &value, JWK &result) -> bool { } } + // The platform key is built once when the material is decoded, so + // verification reuses it. A key that cannot be turned into one stays null and + // simply fails to verify + result.public_key_ = std::move(parsed_key); return true; } diff --git a/vendor/core/src/core/jose/jose_jws_verify_signature.cc b/vendor/core/src/core/jose/jose_jws_verify_signature.cc new file mode 100644 index 00000000..41555a61 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jws_verify_signature.cc @@ -0,0 +1,101 @@ +#include + +#include + +#include // std::optional +#include // std::string_view +#include // std::unreachable + +namespace { + +auto hash_for(const sourcemeta::core::JWSAlgorithm algorithm) + -> sourcemeta::core::SignatureHashFunction { + using sourcemeta::core::JWSAlgorithm; + using sourcemeta::core::SignatureHashFunction; + switch (algorithm) { + case JWSAlgorithm::RS256: + case JWSAlgorithm::PS256: + case JWSAlgorithm::ES256: + return SignatureHashFunction::SHA256; + case JWSAlgorithm::RS384: + case JWSAlgorithm::PS384: + case JWSAlgorithm::ES384: + return SignatureHashFunction::SHA384; + case JWSAlgorithm::RS512: + case JWSAlgorithm::PS512: + case JWSAlgorithm::ES512: + return SignatureHashFunction::SHA512; + // The Edwards-curve algorithm fixes its own hash, so it never reaches here + case JWSAlgorithm::EdDSA: + break; + } + + std::unreachable(); +} + +} // namespace + +namespace sourcemeta::core { + +auto jws_verify_signature(const std::optional algorithm, + const std::string_view signing_input, + const std::string_view signature, const JWK &key) + -> bool { + if (!algorithm.has_value()) { + return false; + } + + // A key that names an algorithm must not contradict the one in use (RFC 7517 + // Section 4.4) + if (key.algorithm().has_value() && + key.algorithm().value() != algorithm.value()) { + return false; + } + + // The key material is parsed into a reusable platform key when the key is + // constructed, so an absent one is material that never formed a valid key + const auto *public_key{key.public_key()}; + if (public_key == nullptr) { + return false; + } + + switch (algorithm.value()) { + case JWSAlgorithm::RS256: + case JWSAlgorithm::RS384: + case JWSAlgorithm::RS512: + return key.type() == JWK::Type::RSA && + rsassa_pkcs1_v15_verify(*public_key, hash_for(algorithm.value()), + signing_input, signature); + case JWSAlgorithm::PS256: + case JWSAlgorithm::PS384: + case JWSAlgorithm::PS512: + return key.type() == JWK::Type::RSA && + rsassa_pss_verify(*public_key, hash_for(algorithm.value()), + signing_input, signature); + // Each ECDSA algorithm is pinned to exactly one curve (RFC 7518 Section + // 3.4), so the key's curve is checked independently of any algorithm it + // declares + case JWSAlgorithm::ES256: + return key.type() == JWK::Type::EllipticCurve && key.curve() == "P-256" && + ecdsa_verify(*public_key, SignatureHashFunction::SHA256, + signing_input, signature); + case JWSAlgorithm::ES384: + return key.type() == JWK::Type::EllipticCurve && key.curve() == "P-384" && + ecdsa_verify(*public_key, SignatureHashFunction::SHA384, + signing_input, signature); + case JWSAlgorithm::ES512: + return key.type() == JWK::Type::EllipticCurve && key.curve() == "P-521" && + ecdsa_verify(*public_key, SignatureHashFunction::SHA512, + signing_input, signature); + // The Edwards-curve algorithm names one of two curves through the key + // rather than the algorithm (RFC 8037 Section 3.1), and the key fixes the + // curve when it is parsed + case JWSAlgorithm::EdDSA: + return key.type() == JWK::Type::OctetKeyPair && + eddsa_verify(*public_key, signing_input, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt.cc b/vendor/core/src/core/jose/jose_jwt.cc new file mode 100644 index 00000000..20bd6e1d --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt.cc @@ -0,0 +1,197 @@ +#include + +#include +#include +#include + +#include // std::chrono::duration, std::chrono::system_clock +#include // std::optional, std::nullopt +#include // std::out_of_range +#include // std::string_view +#include // std::move + +namespace { + +const auto HASH_ALG{sourcemeta::core::JSON::Object::hash("alg")}; +const auto HASH_CRIT{sourcemeta::core::JSON::Object::hash("crit")}; +const auto HASH_KID{sourcemeta::core::JSON::Object::hash("kid")}; +const auto HASH_TYP{sourcemeta::core::JSON::Object::hash("typ")}; +const auto HASH_ISS{sourcemeta::core::JSON::Object::hash("iss")}; +const auto HASH_SUB{sourcemeta::core::JSON::Object::hash("sub")}; +const auto HASH_AUD{sourcemeta::core::JSON::Object::hash("aud")}; +const auto HASH_EXP{sourcemeta::core::JSON::Object::hash("exp")}; +const auto HASH_NBF{sourcemeta::core::JSON::Object::hash("nbf")}; +const auto HASH_IAT{sourcemeta::core::JSON::Object::hash("iat")}; +const auto HASH_JTI{sourcemeta::core::JSON::Object::hash("jti")}; + +auto string_claim(const sourcemeta::core::JSON &object, + const sourcemeta::core::JSON::StringView name, + const sourcemeta::core::JSON::Object::hash_type hash) + -> std::optional { + const auto *member{object.try_at(name, hash)}; + if (member == nullptr || !member->is_string()) { + return std::nullopt; + } + + return std::string_view{member->to_string()}; +} + +auto date_claim(const sourcemeta::core::JSON &object, + const sourcemeta::core::JSON::StringView name, + const sourcemeta::core::JSON::Object::hash_type hash) + -> std::optional { + const auto *member{object.try_at(name, hash)}; + if (member == nullptr || !member->is_number()) { + return std::nullopt; + } + + // A NumericDate is the number of seconds since the Unix epoch, possibly + // non-integer (RFC 7519 Section 2). A decimal-backed number (such as the + // exponent form "1e9") whose magnitude exceeds the range of a double cannot + // stand for a usable timestamp, and untrusted input must not abort + double seconds{0}; + try { + seconds = member->as_real(); + } catch (const std::out_of_range &) { + return std::nullopt; + } + + return sourcemeta::core::from_unix_timestamp( + std::chrono::duration{seconds}); +} + +} // namespace + +namespace sourcemeta::core { + +auto JWT::parse(const std::string_view input, JWT &result) -> bool { + // The compact serialization is exactly three base64url segments joined by + // dots (RFC 7515 Section 7.1) + const auto first{split_once(input, '.')}; + if (!first.has_value()) { + return false; + } + + const auto second{split_once(first->second, '.')}; + if (!second.has_value()) { + return false; + } + + const auto header_segment{first->first}; + const auto payload_segment{second->first}; + const auto signature_segment{second->second}; + if (signature_segment.find('.') != std::string_view::npos) { + return false; + } + + auto header_bytes{base64url_decode(header_segment)}; + auto payload_bytes{base64url_decode(payload_segment)}; + auto signature_bytes{base64url_decode(signature_segment)}; + if (!header_bytes.has_value() || !payload_bytes.has_value() || + !signature_bytes.has_value()) { + return false; + } + + auto header_json{try_parse_json(header_bytes.value())}; + auto payload_json{try_parse_json(payload_bytes.value())}; + if (!header_json.has_value() || !header_json.value().is_object() || + !payload_json.has_value() || !payload_json.value().is_object()) { + return false; + } + + // The algorithm header parameter is required and must be a string (RFC 7515 + // Section 4.1.1) + const auto *algorithm{header_json.value().try_at("alg", HASH_ALG)}; + if (algorithm == nullptr || !algorithm->is_string()) { + return false; + } + + // Critical header extensions are not understood and must be rejected (RFC + // 7515 Section 4.1.11) + if (header_json.value().try_at("crit", HASH_CRIT) != nullptr) { + return false; + } + + result.algorithm_ = to_jws_algorithm(algorithm->to_string()); + result.signing_input_ = + input.substr(0, header_segment.size() + payload_segment.size() + 1); + result.signature_ = std::move(signature_bytes).value(); + result.header_ = std::move(header_json).value(); + result.payload_ = std::move(payload_json).value(); + return true; +} + +JWT::JWT(const std::string_view input) { + if (!parse(input, *this)) { + throw JWTParseError{}; + } +} + +auto JWT::from(const std::string_view input) -> std::optional { + JWT result; + if (parse(input, result)) { + return result; + } + + return std::nullopt; +} + +auto JWT::key_id() const noexcept -> std::optional { + return string_claim(this->header_, "kid", HASH_KID); +} + +auto JWT::type() const noexcept -> std::optional { + return string_claim(this->header_, "typ", HASH_TYP); +} + +auto JWT::issuer() const noexcept -> std::optional { + return string_claim(this->payload_, "iss", HASH_ISS); +} + +auto JWT::subject() const noexcept -> std::optional { + return string_claim(this->payload_, "sub", HASH_SUB); +} + +auto JWT::token_id() const noexcept -> std::optional { + return string_claim(this->payload_, "jti", HASH_JTI); +} + +auto JWT::has_audience(const std::string_view audience) const noexcept -> bool { + const auto *member{this->payload_.try_at("aud", HASH_AUD)}; + if (member == nullptr) { + return false; + } + + // The audience claim is either a single string or an array of strings (RFC + // 7519 Section 4.1.3) + if (member->is_string()) { + return member->to_string() == audience; + } + + if (member->is_array()) { + for (const auto &element : member->as_array()) { + if (element.is_string() && element.to_string() == audience) { + return true; + } + } + } + + return false; +} + +auto JWT::expires_at() const + -> std::optional { + return date_claim(this->payload_, "exp", HASH_EXP); +} + +auto JWT::not_before() const + -> std::optional { + return date_claim(this->payload_, "nbf", HASH_NBF); +} + +auto JWT::issued_at() const + -> std::optional { + return date_claim(this->payload_, "iat", HASH_IAT); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt_check_claims.cc b/vendor/core/src/core/jose/jose_jwt_check_claims.cc new file mode 100644 index 00000000..20ba883e --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt_check_claims.cc @@ -0,0 +1,69 @@ +#include + +#include // std::chrono::seconds, std::chrono::system_clock +#include // std::optional, std::nullopt +#include // std::string_view + +namespace sourcemeta::core { + +auto jwt_check_claims(const JWT &token, const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew, + const std::optional expected_subject) + -> std::optional { + // The issuer must be present and match the expected value (RFC 7519 Section + // 4.1.1) + const auto issuer{token.issuer()}; + if (!issuer.has_value() || issuer.value() != expected_issuer) { + return JWTClaimError::Issuer; + } + + // The subject is checked only when the caller pins one, since it is the + // authenticated principal that many flows accept as any valid identity (RFC + // 7519 Section 4.1.2) + if (expected_subject.has_value()) { + const auto subject{token.subject()}; + if (!subject.has_value() || subject.value() != expected_subject.value()) { + return JWTClaimError::Subject; + } + } + + // The audience must be present and contain the expected value (RFC 7519 + // Section 4.1.3) + if (!token.has_audience(expected_audience)) { + return JWTClaimError::Audience; + } + + // A bearer credential without an expiry is not acceptable for authentication, + // so the claim is required here even though RFC 7519 makes it optional in + // general (RFC 9068 Section 2.2) + const auto expires_at{token.expires_at()}; + if (!expires_at.has_value() || now >= expires_at.value() + clock_skew) { + return JWTClaimError::Expiration; + } + + // The not-before time, when present, must be a usable NumericDate that is not + // in the future. A claim that is present but malformed fails closed rather + // than being ignored (RFC 7519 Section 4.1.5) + const auto &payload{token.payload()}; + if (payload.defines("nbf")) { + const auto not_before{token.not_before()}; + if (!not_before.has_value() || now < not_before.value() - clock_skew) { + return JWTClaimError::NotBefore; + } + } + + // The issued-at time, when present, must be a usable NumericDate that is not + // in the future (RFC 7519 Section 4.1.6) + if (payload.defines("iat")) { + const auto issued_at{token.issued_at()}; + if (!issued_at.has_value() || now < issued_at.value() - clock_skew) { + return JWTClaimError::IssuedAt; + } + } + + return std::nullopt; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt_verify.cc b/vendor/core/src/core/jose/jose_jwt_verify.cc new file mode 100644 index 00000000..1590999b --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt_verify.cc @@ -0,0 +1,126 @@ +#include + +#include + +#include // std::ranges::find +#include // std::chrono::seconds, std::chrono::system_clock +#include // std::optional, std::nullopt +#include // std::span +#include // std::string_view +#include // std::unreachable + +namespace { + +// RFC 7519 Section 5.1: "a recipient using the media type value MUST treat it +// as if `application/` were prepended to any `typ` value not containing a `/`". +// Removing an explicit `application/` prefix, when nothing else in the value +// contains a slash, lets the compact `at+jwt` form and the full +// `application/at+jwt` form compare equal +auto strip_application_prefix(const std::string_view value) + -> std::string_view { + constexpr std::string_view prefix{"application/"}; + if (value.size() > prefix.size() && + sourcemeta::core::equals_ignore_case(value.substr(0, prefix.size()), + prefix) && + value.find('/', prefix.size()) == std::string_view::npos) { + return value.substr(prefix.size()); + } + + return value; +} + +auto to_verification_error(const sourcemeta::core::JWTClaimError error) + -> sourcemeta::core::JWTVerificationError { + using sourcemeta::core::JWTClaimError; + using sourcemeta::core::JWTVerificationError; + switch (error) { + case JWTClaimError::Issuer: + return JWTVerificationError::Issuer; + case JWTClaimError::Subject: + return JWTVerificationError::Subject; + case JWTClaimError::Audience: + return JWTVerificationError::Audience; + case JWTClaimError::Expiration: + return JWTVerificationError::Expiration; + case JWTClaimError::NotBefore: + return JWTVerificationError::NotBefore; + case JWTClaimError::IssuedAt: + return JWTVerificationError::IssuedAt; + } + + std::unreachable(); +} + +} // namespace + +namespace sourcemeta::core { + +auto jwt_verify(const JWT &token, const JWKS &keys, + const std::span allowed_algorithms, + const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew, + const std::optional expected_subject, + const std::optional expected_type) + -> std::optional { + // The algorithm allow-list is enforced before any key is touched, per step 3 + // of the Sourcemeta One validation algorithm + const auto algorithm{token.algorithm()}; + if (!algorithm.has_value() || + std::ranges::find(allowed_algorithms, algorithm.value()) == + allowed_algorithms.end()) { + return JWTVerificationError::AlgorithmNotAllowed; + } + + // A token names its key through `kid` (RFC 7515 Section 4.1.4). When it does + // not, every key in the set is tried, since some providers omit it when they + // publish a single key. A missing or non-verifying key is reported as unknown + // rather than as a signature failure so that downstream can refetch the set, + // except when the named key is present but its signature does not verify + const auto key_id{token.key_id()}; + if (key_id.has_value()) { + const auto *key{keys.find(key_id.value())}; + if (key == nullptr) { + return JWTVerificationError::UnknownKey; + } + + if (!jwt_verify_signature(token, *key)) { + return JWTVerificationError::Signature; + } + } else { + bool verified{false}; + for (const auto &key : keys) { + if (jwt_verify_signature(token, key)) { + verified = true; + break; + } + } + + if (!verified) { + return JWTVerificationError::UnknownKey; + } + } + + // The type is a header concern checked only on an authenticated token, which + // is how the access token profile is enforced (RFC 9068 Section 2.1) + if (expected_type.has_value()) { + const auto type{token.type()}; + if (!type.has_value() || + !equals_ignore_case(strip_application_prefix(type.value()), + strip_application_prefix(expected_type.value()))) { + return JWTVerificationError::Type; + } + } + + const auto claim_error{jwt_check_claims(token, expected_issuer, + expected_audience, now, clock_skew, + expected_subject)}; + if (claim_error.has_value()) { + return to_verification_error(claim_error.value()); + } + + return std::nullopt; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt_verify_signature.cc b/vendor/core/src/core/jose/jose_jwt_verify_signature.cc new file mode 100644 index 00000000..a0995879 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt_verify_signature.cc @@ -0,0 +1,10 @@ +#include + +namespace sourcemeta::core { + +auto jwt_verify_signature(const JWT &token, const JWK &key) -> bool { + return jws_verify_signature(token.algorithm(), token.signing_input(), + token.signature(), key); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json.h b/vendor/core/src/core/json/include/sourcemeta/core/json.h index eec6bb84..f294776f 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json.h @@ -19,6 +19,7 @@ #include // std::basic_ifstream #include // std::initializer_list #include // std::basic_istream +#include // std::optional #include // std::basic_ostream #include // std::ostringstream #include // std::basic_string @@ -75,6 +76,25 @@ SOURCEMETA_CORE_JSON_EXPORT auto parse_json( const std::basic_string_view input) -> JSON; +/// @ingroup json +/// +/// Create a JSON document from a JSON string, returning no value instead of +/// throwing when the input is not valid JSON. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto document{sourcemeta::core::try_parse_json("[ 1, 2, 3 ]")}; +/// assert(document.has_value()); +/// assert(document.value().is_array()); +/// assert(!sourcemeta::core::try_parse_json("[ 1, 2,").has_value()); +/// ``` +SOURCEMETA_CORE_JSON_EXPORT +auto try_parse_json( + const std::basic_string_view input) + -> std::optional; + /// @ingroup json /// /// Create a JSON document from a C++ standard input stream, passing your own diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h index e862de2a..392bae36 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h @@ -21,9 +21,11 @@ #include // std::int64_t, std::uint8_t #include // std::less, std::reference_wrapper, std::function #include // std::initializer_list +#include // std::numeric_limits #include // std::allocator #include // std::set #include // std::basic_istringstream +#include // std::out_of_range #include // std::basic_string, std::char_traits #include // std::basic_string_view #include // std::is_same_v, std::remove_cvref_t @@ -774,7 +776,9 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { } /// Get the JSON numeric document as a real number if it is not one already. - /// For example: + /// A decimal whose magnitude does not fit in a double throws + /// `std::out_of_range`, matching the behaviour of converting that decimal + /// directly. For example: /// /// ```cpp /// #include @@ -783,16 +787,21 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// const sourcemeta::core::JSON document{5}; /// assert(document.as_real() == 5.0); /// ``` - [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_real() const noexcept - -> Real { + [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_real() const -> Real { assert(this->is_number()); - return this->is_real() ? this->to_real() - : static_cast(this->to_integer()); + if (this->is_real()) { + return this->to_real(); + } else if (this->is_integer()) { + return static_cast(this->to_integer()); + } else { + return this->to_decimal().to_double(); + } } /// Get the JSON numeric document as an integer number if it is not one - /// already. If the number is a real number, truncation will take place. For - /// example: + /// already. If the number is a real number, truncation will take place. A + /// value whose magnitude does not fit in a 64-bit integer throws + /// `std::out_of_range`. For example: /// /// ```cpp /// #include @@ -801,13 +810,28 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// const sourcemeta::core::JSON document{5.3}; /// assert(document.as_integer() == 5); /// ``` - [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_integer() const noexcept + [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_integer() const -> Integer { assert(this->is_number()); if (this->is_integer()) { return this->to_integer(); + } else if (this->is_real()) { + const auto truncated{std::trunc(this->to_real())}; + if (truncated < static_cast(std::numeric_limits::min()) || + truncated >= static_cast(std::numeric_limits::max())) { + throw std::out_of_range{ + "The real number does not fit in a 64-bit integer"}; + } + + return static_cast(truncated); } else { - return static_cast(std::trunc(this->to_real())); + const auto integral{this->to_decimal().to_integral()}; + if (!integral.is_int64()) { + throw std::out_of_range{ + "The decimal number does not fit in a 64-bit integer"}; + } + + return integral.to_int64(); } } diff --git a/vendor/core/src/core/json/json.cc b/vendor/core/src/core/json/json.cc index d4229a7f..27193d0c 100644 --- a/vendor/core/src/core/json/json.cc +++ b/vendor/core/src/core/json/json.cc @@ -8,54 +8,79 @@ #include "parser.h" #include "stringify.h" -#include // assert -#include // std::uint64_t -#include // std::filesystem -#include // std::basic_istream -#include // std::numeric_limits -#include // std::basic_ostream -#include // std::cmp_greater -#include // std::vector +#include // assert +#include // std::uint64_t +#include // std::filesystem +#include // std::basic_istream +#include // std::numeric_limits +#include // std::optional, std::nullopt +#include // std::basic_ostream +#include // std::conditional_t +#include // std::cmp_greater +#include // std::vector namespace sourcemeta::core { +template static auto internal_parse_json(const char *&cursor, const char *end, std::uint64_t &line, std::uint64_t &column, const JSON::ParseCallback &callback, const bool track_positions, JSON &output) - -> void { + -> std::conditional_t { const char *buffer_start{cursor}; // Tape entries address the input with 32-bit offsets and lengths, so a larger // input cannot be represented without truncation if (std::cmp_greater(end - cursor, std::numeric_limits::max())) { - throw JSONParseError(line, column); + if constexpr (should_throw) { + throw JSONParseError(line, column); + } else { + return false; + } } std::vector tape; tape.reserve(static_cast(end - cursor) / 8); - if (callback || track_positions) { - scan_json(cursor, end, buffer_start, line, column, tape); + + if constexpr (should_throw) { + if (callback || track_positions) { + scan_json(cursor, end, buffer_start, line, column, tape); + } else { + // Re-scan with position tracking on failure for a precise error message + try { + scan_json(cursor, end, buffer_start, line, column, tape); + } catch (const JSONParseError &) { + cursor = buffer_start; + tape.clear(); + line = 1; + column = 0; + scan_json(cursor, end, buffer_start, line, column, tape); + } + } + construct_json(buffer_start, tape, callback, output); } else { + // Both the scanning and the construction phases signal failure by throwing, + // so a single boundary around them reports either as no value try { - scan_json(cursor, end, buffer_start, line, column, tape); + if (callback || track_positions) { + scan_json(cursor, end, buffer_start, line, column, tape); + } else { + scan_json(cursor, end, buffer_start, line, column, tape); + } + construct_json(buffer_start, tape, callback, output); } catch (const JSONParseError &) { - cursor = buffer_start; - tape.clear(); - line = 1; - column = 0; - scan_json(cursor, end, buffer_start, line, column, tape); + return false; } + return true; } - construct_json(buffer_start, tape, callback, output); } static auto internal_parse_json(const char *&cursor, const char *end, std::uint64_t &line, std::uint64_t &column, const bool track_positions) -> JSON { JSON output{nullptr}; - internal_parse_json(cursor, end, line, column, nullptr, track_positions, - output); + internal_parse_json(cursor, end, line, column, nullptr, track_positions, + output); return output; } @@ -110,6 +135,21 @@ auto parse_json( false); } +auto try_parse_json( + const std::basic_string_view input) + -> std::optional { + std::uint64_t line{1}; + std::uint64_t column{0}; + const char *cursor{input.empty() ? "" : input.data()}; + JSON output{nullptr}; + if (internal_parse_json(cursor, cursor + input.size(), line, column, + nullptr, false, output)) { + return output; + } + + return std::nullopt; +} + auto read_json(const std::filesystem::path &path) -> JSON { try { return parse_json(read_file_to_string(path)); @@ -127,7 +167,7 @@ auto parse_json(std::basic_istream &stream, const auto input{read_to_string(stream)}; const char *cursor{input.data()}; const char *end{input.data() + input.size()}; - internal_parse_json(cursor, end, line, column, callback, true, output); + internal_parse_json(cursor, end, line, column, callback, true, output); if (start_position != static_cast(-1)) { const auto consumed{static_cast(cursor - input.data())}; stream.clear(); @@ -140,8 +180,8 @@ auto parse_json( std::uint64_t &line, std::uint64_t &column, JSON &output, const JSON::ParseCallback &callback) -> void { const char *cursor{input.empty() ? "" : input.data()}; - internal_parse_json(cursor, cursor + input.size(), line, column, callback, - true, output); + internal_parse_json(cursor, cursor + input.size(), line, column, + callback, true, output); } // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) @@ -153,7 +193,7 @@ auto parse_json(std::basic_istream &stream, const char *end{input.data() + input.size()}; std::uint64_t line{1}; std::uint64_t column{0}; - internal_parse_json(cursor, end, line, column, callback, false, output); + internal_parse_json(cursor, end, line, column, callback, false, output); if (start_position != static_cast(-1)) { const auto consumed{static_cast(cursor - input.data())}; stream.clear(); @@ -167,8 +207,8 @@ auto parse_json( std::uint64_t line{1}; std::uint64_t column{0}; const char *cursor{input.empty() ? "" : input.data()}; - internal_parse_json(cursor, cursor + input.size(), line, column, callback, - false, output); + internal_parse_json(cursor, cursor + input.size(), line, column, + callback, false, output); } auto read_json(const std::filesystem::path &path, JSON &output, diff --git a/vendor/core/src/core/json/json_value.cc b/vendor/core/src/core/json/json_value.cc index 5231525f..26f9dfa0 100644 --- a/vendor/core/src/core/json/json_value.cc +++ b/vendor/core/src/core/json/json_value.cc @@ -432,6 +432,9 @@ auto JSON::size(const String &value) noexcept -> std::size_t { return result; } +// `as_real` is reached only for integer and real operands here, never a +// decimal, so it cannot throw even though it is no longer noexcept +// NOLINTNEXTLINE(bugprone-exception-escape) auto JSON::operator<(const JSON &other) const noexcept -> bool { if ((this->type() == Type::Integer && other.type() == Type::Real) || (this->type() == Type::Real && other.type() == Type::Integer)) { @@ -489,6 +492,9 @@ auto JSON::operator>=(const JSON &other) const noexcept -> bool { return *this > other || *this == other; } +// `as_real` is reached only for integer and real operands here, never a +// decimal, so it cannot throw even though it is no longer noexcept +// NOLINTNEXTLINE(bugprone-exception-escape) auto JSON::operator==(const JSON &other) const noexcept -> bool { if ((this->type() == Type::Integer && other.type() == Type::Real) || (this->type() == Type::Real && other.type() == Type::Integer)) { diff --git a/vendor/core/src/lang/numeric/include/sourcemeta/core/numeric_uint128.h b/vendor/core/src/lang/numeric/include/sourcemeta/core/numeric_uint128.h index df07a78d..aea02f47 100644 --- a/vendor/core/src/lang/numeric/include/sourcemeta/core/numeric_uint128.h +++ b/vendor/core/src/lang/numeric/include/sourcemeta/core/numeric_uint128.h @@ -55,17 +55,35 @@ struct uint128_t { return *this; } + auto operator-=(const uint128_t &other) noexcept -> uint128_t & { + const auto old_low = this->low; + this->low -= other.low; + this->high -= other.high + (old_low < other.low ? 1 : 0); + return *this; + } + auto operator*=(const uint128_t &other) noexcept -> uint128_t & { *this = *this * other; return *this; } + auto operator%=(const uint128_t &other) noexcept -> uint128_t & { + *this = *this % other; + return *this; + } + friend auto operator+(uint128_t left, const uint128_t &right) noexcept -> uint128_t { left += right; return left; } + friend auto operator-(uint128_t left, const uint128_t &right) noexcept + -> uint128_t { + left -= right; + return left; + } + friend auto operator*(const uint128_t &left, const uint128_t &right) noexcept -> uint128_t { std::uint64_t result_high; diff --git a/vendor/core/src/lang/text/include/sourcemeta/core/text.h b/vendor/core/src/lang/text/include/sourcemeta/core/text.h index 1f6e3749..a5dab4ff 100644 --- a/vendor/core/src/lang/text/include/sourcemeta/core/text.h +++ b/vendor/core/src/lang/text/include/sourcemeta/core/text.h @@ -398,6 +398,22 @@ auto hex_to_bytes(const std::string_view input, SOURCEMETA_CORE_TEXT_EXPORT auto bytes_to_hex(const std::string_view input) -> std::string; +/// @ingroup text +/// +/// Return whether two strings are equal under ASCII case-insensitive +/// comparison. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::equals_ignore_case("Hello", "hELLO")); +/// assert(!sourcemeta::core::equals_ignore_case("foo", "bar")); +/// ``` +SOURCEMETA_CORE_TEXT_EXPORT +auto equals_ignore_case(const std::string_view left, + const std::string_view right) noexcept -> bool; + /// @ingroup text /// /// Return `input` with `suffix` removed from the end under ASCII diff --git a/vendor/core/src/lang/text/text.cc b/vendor/core/src/lang/text/text.cc index 52df89d0..6c2c6efa 100644 --- a/vendor/core/src/lang/text/text.cc +++ b/vendor/core/src/lang/text/text.cc @@ -174,6 +174,21 @@ auto split_once(const std::string_view input, return std::pair{before, after}; } +auto equals_ignore_case(const std::string_view left, + const std::string_view right) noexcept -> bool { + if (left.size() != right.size()) { + return false; + } + + for (std::size_t index{0}; index < left.size(); ++index) { + if (to_lowercase(left[index]) != to_lowercase(right[index])) { + return false; + } + } + + return true; +} + auto remove_suffix_ignore_case(const std::string_view input, const std::string_view suffix) noexcept -> std::string_view { diff --git a/vendor/jsonbinpack/DEPENDENCIES b/vendor/jsonbinpack/DEPENDENCIES index 2c078a71..7947a872 100644 --- a/vendor/jsonbinpack/DEPENDENCIES +++ b/vendor/jsonbinpack/DEPENDENCIES @@ -1,4 +1,4 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core df8f2970ccf85a3a3f01e004ac436ff916f8c52a -blaze https://github.com/sourcemeta/blaze 7b214cff6d575831c16a2ce33f55d97c02eb6338 +core https://github.com/sourcemeta/core bb1c78e8fa148a2ece951bb776798a43fe328821 +blaze https://github.com/sourcemeta/blaze 04832d45bf4327d4ec874fa67f339797cd49b375 bootstrap https://github.com/twbs/bootstrap 1a6fdfae6be09b09eaced8f0e442ca6f7680a61e