From 77d867fd4bec639c2cd3c3987c27f09e5711e729 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Mon, 15 Jun 2026 15:53:49 -0400 Subject: [PATCH] Support `dependentSchemas` and `dependentRequired` on `codegen` Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 4 +- docs/codegen.markdown | 4 +- .../dependent_required_to_any_of.h | 20 +- .../dependent_schemas_to_any_of.h | 20 +- .../canonicalizer/type_array_to_any_of.h | 23 +- vendor/core/CMakeLists.txt | 11 +- vendor/core/config.cmake.in | 22 +- vendor/core/src/core/crypto/CMakeLists.txt | 39 +- vendor/core/src/core/crypto/crypto_base64.cc | 194 ++++++++++ vendor/core/src/core/crypto/crypto_bignum.h | 355 ++++++++++++++++++ vendor/core/src/core/crypto/crypto_ecc.h | 206 ++++++++++ vendor/core/src/core/crypto/crypto_fnv128.cc | 15 +- vendor/core/src/core/crypto/crypto_helpers.h | 90 +++++ vendor/core/src/core/crypto/crypto_random.h | 14 + .../src/core/crypto/crypto_random_apple.cc | 19 + .../src/core/crypto/crypto_random_openssl.cc | 16 + .../src/core/crypto/crypto_random_other.cc | 19 + .../src/core/crypto/crypto_random_windows.cc | 22 ++ vendor/core/src/core/crypto/crypto_sha1.cc | 215 +---------- .../core/src/core/crypto/crypto_sha1_apple.cc | 42 +++ .../src/core/crypto/crypto_sha1_openssl.cc | 36 ++ .../core/src/core/crypto/crypto_sha1_other.cc | 161 ++++++++ .../src/core/crypto/crypto_sha1_windows.cc | 65 ++++ vendor/core/src/core/crypto/crypto_sha256.cc | 241 +----------- .../src/core/crypto/crypto_sha256_apple.cc | 36 ++ .../src/core/crypto/crypto_sha256_openssl.cc | 35 ++ .../src/core/crypto/crypto_sha256_other.cc | 185 +++++++++ .../src/core/crypto/crypto_sha256_windows.cc | 64 ++++ vendor/core/src/core/crypto/crypto_sha2_64.h | 200 ++++++++++ vendor/core/src/core/crypto/crypto_sha384.cc | 21 ++ .../src/core/crypto/crypto_sha384_apple.cc | 36 ++ .../src/core/crypto/crypto_sha384_openssl.cc | 35 ++ .../src/core/crypto/crypto_sha384_other.cc | 35 ++ .../src/core/crypto/crypto_sha384_windows.cc | 64 ++++ vendor/core/src/core/crypto/crypto_sha512.cc | 21 ++ .../src/core/crypto/crypto_sha512_apple.cc | 36 ++ .../src/core/crypto/crypto_sha512_openssl.cc | 35 ++ .../src/core/crypto/crypto_sha512_other.cc | 33 ++ .../src/core/crypto/crypto_sha512_windows.cc | 64 ++++ vendor/core/src/core/crypto/crypto_uuid.cc | 49 +-- .../core/crypto/crypto_verify_ecdsa_apple.cc | 135 +++++++ .../crypto/crypto_verify_ecdsa_openssl.cc | 161 ++++++++ .../core/crypto/crypto_verify_ecdsa_other.cc | 119 ++++++ .../crypto/crypto_verify_ecdsa_windows.cc | 107 ++++++ .../core/crypto/crypto_verify_rsa_apple.cc | 150 ++++++++ .../core/crypto/crypto_verify_rsa_openssl.cc | 144 +++++++ .../core/crypto/crypto_verify_rsa_other.cc | 258 +++++++++++++ .../core/crypto/crypto_verify_rsa_windows.cc | 137 +++++++ .../crypto/include/sourcemeta/core/crypto.h | 6 +- .../include/sourcemeta/core/crypto_base64.h | 104 +++++ .../include/sourcemeta/core/crypto_sha256.h | 15 + .../include/sourcemeta/core/crypto_sha384.h | 59 +++ .../include/sourcemeta/core/crypto_sha512.h | 59 +++ .../include/sourcemeta/core/crypto_verify.h | 84 +++++ vendor/core/src/core/http/CMakeLists.txt | 2 +- .../core/http/include/sourcemeta/core/http.h | 18 + vendor/core/src/core/http/parse_bearer.cc | 68 ++++ vendor/core/src/core/jose/CMakeLists.txt | 12 + .../core/jose/include/sourcemeta/core/jose.h | 24 ++ .../include/sourcemeta/core/jose_algorithm.h | 47 +++ .../jose/include/sourcemeta/core/jose_error.h | 40 ++ .../jose/include/sourcemeta/core/jose_jwk.h | 115 ++++++ .../jose/include/sourcemeta/core/jose_jwks.h | 83 ++++ vendor/core/src/core/jose/jose_algorithm.cc | 33 ++ vendor/core/src/core/jose/jose_jwk.cc | 202 ++++++++++ vendor/core/src/core/jose/jose_jwks.cc | 72 ++++ vendor/core/src/core/time/CMakeLists.txt | 5 +- .../core/time/include/sourcemeta/core/time.h | 39 ++ vendor/core/src/core/time/unix_timestamp.cc | 41 ++ .../src/lang/io/include/sourcemeta/core/io.h | 13 +- .../lang/text/include/sourcemeta/core/text.h | 81 ++++ vendor/core/src/lang/text/text.cc | 87 +++++ 72 files changed, 4779 insertions(+), 543 deletions(-) create mode 100644 vendor/core/src/core/crypto/crypto_base64.cc create mode 100644 vendor/core/src/core/crypto/crypto_bignum.h create mode 100644 vendor/core/src/core/crypto/crypto_ecc.h create mode 100644 vendor/core/src/core/crypto/crypto_helpers.h create mode 100644 vendor/core/src/core/crypto/crypto_random.h create mode 100644 vendor/core/src/core/crypto/crypto_random_apple.cc create mode 100644 vendor/core/src/core/crypto/crypto_random_openssl.cc create mode 100644 vendor/core/src/core/crypto/crypto_random_other.cc create mode 100644 vendor/core/src/core/crypto/crypto_random_windows.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha1_apple.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha1_openssl.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha1_other.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha1_windows.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha256_apple.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha256_openssl.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha256_other.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha256_windows.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha2_64.h create mode 100644 vendor/core/src/core/crypto/crypto_sha384.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha384_apple.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha384_openssl.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha384_other.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha384_windows.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha512.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha512_apple.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha512_openssl.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha512_other.cc create mode 100644 vendor/core/src/core/crypto/crypto_sha512_windows.cc create mode 100644 vendor/core/src/core/crypto/crypto_verify_ecdsa_apple.cc create mode 100644 vendor/core/src/core/crypto/crypto_verify_ecdsa_openssl.cc create mode 100644 vendor/core/src/core/crypto/crypto_verify_ecdsa_other.cc create mode 100644 vendor/core/src/core/crypto/crypto_verify_ecdsa_windows.cc create mode 100644 vendor/core/src/core/crypto/crypto_verify_rsa_apple.cc create mode 100644 vendor/core/src/core/crypto/crypto_verify_rsa_openssl.cc create mode 100644 vendor/core/src/core/crypto/crypto_verify_rsa_other.cc create mode 100644 vendor/core/src/core/crypto/crypto_verify_rsa_windows.cc create mode 100644 vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h create mode 100644 vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha384.h create mode 100644 vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha512.h create mode 100644 vendor/core/src/core/crypto/include/sourcemeta/core/crypto_verify.h create mode 100644 vendor/core/src/core/http/parse_bearer.cc create mode 100644 vendor/core/src/core/jose/CMakeLists.txt create mode 100644 vendor/core/src/core/jose/include/sourcemeta/core/jose.h create mode 100644 vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h create mode 100644 vendor/core/src/core/jose/include/sourcemeta/core/jose_error.h create mode 100644 vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h create mode 100644 vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h create mode 100644 vendor/core/src/core/jose/jose_algorithm.cc create mode 100644 vendor/core/src/core/jose/jose_jwk.cc create mode 100644 vendor/core/src/core/jose/jose_jwks.cc create mode 100644 vendor/core/src/core/time/unix_timestamp.cc diff --git a/DEPENDENCIES b/DEPENDENCIES index bbae9f56..3af6a756 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,7 +1,7 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core eb361a9f3a01f7cffb5da7946a9516ee11dc9ede +core https://github.com/sourcemeta/core 04e936961d1e31f6b70fa5a30e115b2f7855674a jsonbinpack https://github.com/sourcemeta/jsonbinpack ac8e1af733a781fc4c94a14157f80970ea569479 -blaze https://github.com/sourcemeta/blaze 208a2b74eb10bd81e5c0a0e24500676f30c6c97d +blaze https://github.com/sourcemeta/blaze e73e5e1667487717b44ab351a0f46e01d507fefb mbedtls https://github.com/Mbed-TLS/mbedtls v3.6.6 curl https://github.com/curl/curl curl-8_20_0 nghttp2 https://github.com/nghttp2/nghttp2 v1.67.1 diff --git a/docs/codegen.markdown b/docs/codegen.markdown index 964f8874..3472c48b 100644 --- a/docs/codegen.markdown +++ b/docs/codegen.markdown @@ -48,7 +48,7 @@ to use a JSON Schema validator at runtime to enforce remaining constraints. | Applicator (2020-12) | `anyOf` | Yes | | Applicator (2020-12) | `patternProperties` | Partial (language limitations) | | Applicator (2020-12) | `propertyNames` | Ignored | -| Applicator (2020-12) | `dependentSchemas` | Pending | +| Applicator (2020-12) | `dependentSchemas` | Yes | | Applicator (2020-12) | `contains` | Ignored | | Applicator (2020-12) | `allOf` | Yes | | Applicator (2020-12) | `oneOf` | Partial (language limitations) | @@ -70,7 +70,7 @@ to use a JSON Schema validator at runtime to enforce remaining constraints. | Validation (2020-12) | `multipleOf` | Ignored | | Validation (2020-12) | `minProperties` | Ignored | | Validation (2020-12) | `maxProperties` | Ignored | -| Validation (2020-12) | `dependentRequired` | Pending | +| Validation (2020-12) | `dependentRequired` | Yes | | Validation (2020-12) | `minItems` | Ignored | | Validation (2020-12) | `maxItems` | Ignored | | Validation (2020-12) | `minContains` | Ignored | diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h b/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h index 2bf641b4..360f38bb 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h +++ b/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h @@ -27,6 +27,15 @@ class DependentRequiredToAnyOf final : public SchemaTransformRule { ONLY_CONTINUE_IF(std::ranges::any_of( dependent_required->as_object(), [](const auto &entry) { return entry.second.is_array(); })); + + if (!vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_2020_12_Applicator})) { + throw SchemaError( + "Cannot canonicalise `dependentRequired` without the Applicator " + "vocabulary"); + } + return true; } @@ -45,19 +54,16 @@ class DependentRequiredToAnyOf final : public SchemaTransformRule { required_all.push_back(dependent); } - auto not_required{JSON::make_object()}; - not_required.assign("type", JSON{"object"}); - not_required.assign("required", JSON::make_array()); - not_required.at("required").push_back(JSON{entry.first}); - auto not_branch{JSON::make_object()}; - not_branch.assign("not", std::move(not_required)); + auto absence_branch{JSON::make_object()}; + absence_branch.assign("properties", JSON::make_object()); + absence_branch.at("properties").assign(entry.first, JSON{false}); auto required_branch{JSON::make_object()}; required_branch.assign("type", JSON{"object"}); required_branch.assign("required", std::move(required_all)); auto pair{JSON::make_array()}; - pair.push_back(std::move(not_branch)); + pair.push_back(std::move(absence_branch)); pair.push_back(std::move(required_branch)); auto wrapper{JSON::make_object()}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h b/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h index 9ea0aa69..989ecb2c 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h +++ b/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h @@ -23,6 +23,15 @@ class DependentSchemasToAnyOf final : public SchemaTransformRule { const auto *dependent_schemas{schema.try_at("dependentSchemas")}; ONLY_CONTINUE_IF(dependent_schemas && dependent_schemas->is_object() && !dependent_schemas->empty()); + + if (!vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_2020_12_Validation})) { + throw SchemaError( + "Cannot canonicalise `dependentSchemas` without the Validation " + "vocabulary"); + } + return true; } @@ -30,12 +39,9 @@ class DependentSchemasToAnyOf final : public SchemaTransformRule { auto result_branches{JSON::make_array()}; for (const auto &entry : schema.at("dependentSchemas").as_object()) { - auto not_required{JSON::make_object()}; - not_required.assign("type", JSON{"object"}); - not_required.assign("required", JSON::make_array()); - not_required.at("required").push_back(JSON{entry.first}); - auto not_branch{JSON::make_object()}; - not_branch.assign("not", std::move(not_required)); + auto absence_branch{JSON::make_object()}; + absence_branch.assign("properties", JSON::make_object()); + absence_branch.at("properties").assign(entry.first, JSON{false}); auto required_obj{JSON::make_object()}; required_obj.assign("type", JSON{"object"}); @@ -50,7 +56,7 @@ class DependentSchemasToAnyOf final : public SchemaTransformRule { allof_branch.assign("allOf", std::move(all_of)); auto pair{JSON::make_array()}; - pair.push_back(std::move(not_branch)); + pair.push_back(std::move(absence_branch)); pair.push_back(std::move(allof_branch)); auto wrapper{JSON::make_object()}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h b/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h index 5dbedf63..99b18626 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h +++ b/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h @@ -18,15 +18,20 @@ class TypeArrayToAnyOf final : public SchemaTransformRule { const sourcemeta::blaze::SchemaResolver &, const bool) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF(vocabularies.contains_any( - {Vocabularies::Known::JSON_Schema_2020_12_Validation, - Vocabularies::Known::JSON_Schema_2020_12_Applicator, - Vocabularies::Known::JSON_Schema_2019_09_Validation, - Vocabularies::Known::JSON_Schema_2019_09_Applicator, - Vocabularies::Known::JSON_Schema_Draft_7, - Vocabularies::Known::JSON_Schema_Draft_6, - Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object()); + ONLY_CONTINUE_IF( + ((vocabularies.contains( + Vocabularies::Known::JSON_Schema_2020_12_Validation) && + vocabularies.contains( + Vocabularies::Known::JSON_Schema_2020_12_Applicator)) || + (vocabularies.contains( + Vocabularies::Known::JSON_Schema_2019_09_Validation) && + vocabularies.contains( + Vocabularies::Known::JSON_Schema_2019_09_Applicator)) || + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4})) && + schema.is_object()); const auto *type{schema.try_at("type")}; ONLY_CONTINUE_IF(type && type->is_array()); diff --git a/vendor/core/CMakeLists.txt b/vendor/core/CMakeLists.txt index 6da6d84d..512e0d3e 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_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) option(SOURCEMETA_CORE_HTML "Build the Sourcemeta Core HTML library" ON) @@ -121,7 +122,7 @@ endif() if(SOURCEMETA_CORE_CRYPTO) if(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) - find_package(OpenSSL REQUIRED) + find_package(OpenSSL 3.0 REQUIRED) endif() add_subdirectory(src/core/crypto) endif() @@ -188,6 +189,10 @@ if(SOURCEMETA_CORE_HTTP) add_subdirectory(src/core/http) endif() +if(SOURCEMETA_CORE_JOSE) + add_subdirectory(src/core/jose) +endif() + if(SOURCEMETA_CORE_SEMVER) add_subdirectory(src/core/semver) endif() @@ -346,6 +351,10 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/http) endif() + if(SOURCEMETA_CORE_JOSE) + add_subdirectory(test/jose) + endif() + if(SOURCEMETA_CORE_SEMVER) add_subdirectory(test/semver) endif() diff --git a/vendor/core/config.cmake.in b/vendor/core/config.cmake.in index 31e34d56..d4511d6c 100644 --- a/vendor/core/config.cmake.in +++ b/vendor/core/config.cmake.in @@ -27,6 +27,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonrpc) list(APPEND SOURCEMETA_CORE_COMPONENTS mcp) list(APPEND SOURCEMETA_CORE_COMPONENTS http) + list(APPEND SOURCEMETA_CORE_COMPONENTS jose) list(APPEND SOURCEMETA_CORE_COMPONENTS semver) list(APPEND SOURCEMETA_CORE_COMPONENTS gzip) list(APPEND SOURCEMETA_CORE_COMPONENTS html) @@ -64,8 +65,11 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_time.cmake") elseif(component STREQUAL "crypto") if(@SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL@) - find_dependency(OpenSSL) + find_dependency(OpenSSL 3.0) 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_text.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_crypto.cmake") elseif(component STREQUAL "regex") find_dependency(PCRE2 CONFIG) @@ -111,6 +115,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_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_crypto.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_gzip.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonl.cmake") @@ -161,11 +166,26 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_time.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_http.cmake") + elseif(component STREQUAL "jose") + if(@SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL@) + find_dependency(OpenSSL 3.0) + 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") + 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_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_crypto.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jose.cmake") elseif(component STREQUAL "semver") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_semver.cmake") elseif(component STREQUAL "gzip") find_dependency(LibDeflate CONFIG) + 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_text.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_crypto.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_gzip.cmake") elseif(component STREQUAL "html") diff --git a/vendor/core/src/core/crypto/CMakeLists.txt b/vendor/core/src/core/crypto/CMakeLists.txt index 865ae778..17c5104a 100644 --- a/vendor/core/src/core/crypto/CMakeLists.txt +++ b/vendor/core/src/core/crypto/CMakeLists.txt @@ -1,12 +1,41 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME crypto - PRIVATE_HEADERS sha256.h sha1.h fnv128.h uuid.h crc32.h - SOURCES crypto_sha256.cc crypto_sha1.cc crypto_fnv128.cc - crypto_uuid.cc crypto_crc32.cc) + PRIVATE_HEADERS sha256.h sha384.h sha512.h sha1.h fnv128.h uuid.h crc32.h + base64.h verify.h + SOURCES crypto_sha256.cc crypto_sha384.cc crypto_sha512.cc crypto_sha1.cc + crypto_uuid.cc crypto_fnv128.cc crypto_crc32.cc crypto_base64.cc + crypto_sha2_64.h crypto_random.h crypto_helpers.h) + +target_link_libraries(sourcemeta_core_crypto + PRIVATE sourcemeta::core::text sourcemeta::core::numeric) if(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) - target_compile_definitions(sourcemeta_core_crypto - PRIVATE 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) target_link_libraries(sourcemeta_core_crypto PRIVATE OpenSSL::Crypto) +elseif(APPLE) + 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) + target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework Security") + target_link_libraries(sourcemeta_core_crypto + PRIVATE "-framework CoreFoundation") +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) + 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) endif() if(SOURCEMETA_CORE_INSTALL) diff --git a/vendor/core/src/core/crypto/crypto_base64.cc b/vendor/core/src/core/crypto/crypto_base64.cc new file mode 100644 index 00000000..1357e438 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_base64.cc @@ -0,0 +1,194 @@ +#include + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint32_t +#include // std::optional, std::nullopt +#include // std::ostream +#include // std::string +#include // std::string_view + +namespace { + +// RFC 4648 Section 4, Table 1: The Base 64 Alphabet +constexpr std::string_view BASE64_ALPHABET{ + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"}; + +// RFC 4648 Section 5, Table 2: The "URL and Filename safe" Base 64 Alphabet +constexpr std::string_view BASE64URL_ALPHABET{ + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"}; + +constexpr std::uint8_t INVALID_SEXTET{0xFF}; + +constexpr auto build_decode_table(const std::string_view alphabet) noexcept + -> std::array { + std::array table{}; + table.fill(INVALID_SEXTET); + for (std::size_t index = 0; index < alphabet.size(); ++index) { + table[static_cast(alphabet[index])] = + static_cast(index); + } + return table; +} + +constexpr std::array BASE64_DECODE_TABLE{ + build_decode_table(BASE64_ALPHABET)}; +constexpr std::array BASE64URL_DECODE_TABLE{ + build_decode_table(BASE64URL_ALPHABET)}; + +auto encode(const std::string_view input, const std::string_view alphabet, + const bool padding, std::string &output) -> void { + std::size_t index{0}; + while (index + 3 <= input.size()) { + const std::uint32_t first{static_cast(input[index])}; + const std::uint32_t second{static_cast(input[index + 1])}; + const std::uint32_t third{static_cast(input[index + 2])}; + output.push_back(alphabet[first >> 2u]); + output.push_back(alphabet[((first & 0x03u) << 4u) | (second >> 4u)]); + output.push_back(alphabet[((second & 0x0Fu) << 2u) | (third >> 6u)]); + output.push_back(alphabet[third & 0x3Fu]); + index += 3; + } + + const auto remaining{input.size() - index}; + if (remaining == 1) { + const std::uint32_t first{static_cast(input[index])}; + output.push_back(alphabet[first >> 2u]); + output.push_back(alphabet[(first & 0x03u) << 4u]); + if (padding) { + output.push_back('='); + output.push_back('='); + } + } else if (remaining == 2) { + const std::uint32_t first{static_cast(input[index])}; + const std::uint32_t second{static_cast(input[index + 1])}; + output.push_back(alphabet[first >> 2u]); + output.push_back(alphabet[((first & 0x03u) << 4u) | (second >> 4u)]); + output.push_back(alphabet[(second & 0x0Fu) << 2u]); + if (padding) { + output.push_back('='); + } + } +} + +auto decode(const std::string_view input, + const std::array &table, const bool padding) + -> std::optional { + auto data{input}; + + if (padding) { + // RFC 4648 Section 4: "Special processing is performed if fewer than 24 + // bits are available at the end of the data being encoded. A full encoding + // quantum is always completed at the end of a quantity", hence the padded + // form must be a multiple of four characters + if (data.size() % 4 != 0) { + return std::nullopt; + } + + if (data.ends_with('=')) { + data.remove_suffix(1); + if (data.ends_with('=')) { + data.remove_suffix(1); + } + } + } + + if (data.size() % 4 == 1) { + return std::nullopt; + } + + std::string output; + output.reserve(((data.size() / 4) * 3) + 2); + + std::size_t index{0}; + while (index + 4 <= data.size()) { + const std::uint32_t first{table[static_cast(data[index])]}; + const std::uint32_t second{ + table[static_cast(data[index + 1])]}; + const std::uint32_t third{ + table[static_cast(data[index + 2])]}; + const std::uint32_t fourth{ + table[static_cast(data[index + 3])]}; + if (first == INVALID_SEXTET || second == INVALID_SEXTET || + third == INVALID_SEXTET || fourth == INVALID_SEXTET) { + return std::nullopt; + } + + const std::uint32_t group{(first << 18u) | (second << 12u) | (third << 6u) | + fourth}; + output.push_back(static_cast((group >> 16u) & 0xFFu)); + output.push_back(static_cast((group >> 8u) & 0xFFu)); + output.push_back(static_cast(group & 0xFFu)); + index += 4; + } + + // RFC 4648 Section 3.5: "Implementations MAY chose to reject the encoding + // if the pad bits have not been set to zero". We reject so that every value + // has exactly one accepted encoding + const auto remaining{data.size() - index}; + if (remaining == 2) { + const std::uint32_t first{table[static_cast(data[index])]}; + const std::uint32_t second{ + table[static_cast(data[index + 1])]}; + if (first == INVALID_SEXTET || second == INVALID_SEXTET || + (second & 0x0Fu) != 0) { + return std::nullopt; + } + + output.push_back(static_cast((first << 2u) | (second >> 4u))); + } else if (remaining == 3) { + const std::uint32_t first{table[static_cast(data[index])]}; + const std::uint32_t second{ + table[static_cast(data[index + 1])]}; + const std::uint32_t third{ + table[static_cast(data[index + 2])]}; + if (first == INVALID_SEXTET || second == INVALID_SEXTET || + third == INVALID_SEXTET || (third & 0x03u) != 0) { + return std::nullopt; + } + + output.push_back(static_cast((first << 2u) | (second >> 4u))); + output.push_back( + static_cast(((second & 0x0Fu) << 4u) | (third >> 2u))); + } + + return output; +} + +} // namespace + +namespace sourcemeta::core { + +auto base64_encode(const std::string_view input, std::ostream &output) -> void { + output << base64_encode(input); +} + +auto base64_encode(const std::string_view input) -> std::string { + std::string result; + result.reserve(((input.size() + 2) / 3) * 4); + encode(input, BASE64_ALPHABET, true, result); + return result; +} + +auto base64_decode(const std::string_view input) -> std::optional { + return decode(input, BASE64_DECODE_TABLE, true); +} + +auto base64url_encode(const std::string_view input, std::ostream &output) + -> void { + output << base64url_encode(input); +} + +auto base64url_encode(const std::string_view input) -> std::string { + std::string result; + result.reserve(((input.size() + 2) / 3) * 4); + encode(input, BASE64URL_ALPHABET, false, result); + return result; +} + +auto base64url_decode(const std::string_view input) + -> std::optional { + return decode(input, BASE64URL_DECODE_TABLE, false); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_bignum.h b/vendor/core/src/core/crypto/crypto_bignum.h new file mode 100644 index 00000000..cd025381 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_bignum.h @@ -0,0 +1,355 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_BIGNUM_H_ +#define SOURCEMETA_CORE_CRYPTO_BIGNUM_H_ + +// 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 + +#include + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint64_t +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +using BignumDoubleWord = uint128_t; + +struct Bignum { + // Enough words for an 8192-bit product plus shifting headroom + static constexpr std::size_t capacity{130}; + std::array words{}; + std::size_t size{0}; +}; + +inline auto bignum_normalize(Bignum &value) noexcept -> void { + while (value.size > 0 && value.words[value.size - 1] == 0) { + value.size -= 1; + } +} + +inline auto bignum_from_bytes(const std::string_view input) noexcept -> Bignum { + Bignum result; + std::size_t bytes_consumed{0}; + for (std::size_t index = input.size(); index > 0; --index) { + const auto byte{static_cast(input[index - 1])}; + const auto word_index{bytes_consumed / 8}; + if (word_index >= Bignum::capacity) { + break; + } + + result.words[word_index] |= static_cast(byte) + << (8 * (bytes_consumed % 8)); + bytes_consumed += 1; + } + + result.size = (bytes_consumed + 7) / 8; + bignum_normalize(result); + return result; +} + +inline auto bignum_from_u64(const std::uint64_t value) noexcept -> Bignum { + Bignum result; + if (value > 0) { + result.words[0] = value; + result.size = 1; + } + + return result; +} + +inline auto bignum_from_hex(const std::string_view hex) noexcept -> Bignum { + const auto nibble{[](const char character) noexcept -> std::uint8_t { + if (character >= '0' && character <= '9') { + return static_cast(character - '0'); + } else if (character >= 'a' && character <= 'f') { + return static_cast(character - 'a' + 10); + } else { + return static_cast(character - 'A' + 10); + } + }}; + + std::string bytes; + bytes.reserve((hex.size() + 1) / 2); + + // An odd length means the leading nibble forms a byte on its own, as if a + // zero had been prepended + std::size_t index{0}; + if (hex.size() % 2 != 0) { + bytes.push_back(static_cast(nibble(hex[0]))); + index = 1; + } + + for (; index + 1 < hex.size(); index += 2) { + bytes.push_back( + static_cast((nibble(hex[index]) << 4u) | nibble(hex[index + 1]))); + } + + return bignum_from_bytes(bytes); +} + +inline auto bignum_is_zero(const Bignum &value) noexcept -> bool { + return value.size == 0; +} + +inline auto bignum_compare(const Bignum &left, const Bignum &right) noexcept + -> int { + if (left.size != right.size) { + return left.size < right.size ? -1 : 1; + } + + for (std::size_t index = left.size; index > 0; --index) { + if (left.words[index - 1] != right.words[index - 1]) { + return left.words[index - 1] < right.words[index - 1] ? -1 : 1; + } + } + + return 0; +} + +inline auto bignum_bit_length(const Bignum &value) noexcept -> std::size_t { + if (value.size == 0) { + return 0; + } + + auto top_word{value.words[value.size - 1]}; + std::size_t top_bits{0}; + while (top_word > 0) { + top_word >>= 1u; + top_bits += 1; + } + + return ((value.size - 1) * 64) + top_bits; +} + +inline auto bignum_get_bit(const Bignum &value, const std::size_t bit) noexcept + -> bool { + return ((value.words[bit / 64] >> (bit % 64)) & 1u) != 0; +} + +// Assumes the result fits in the capacity +inline auto bignum_shift_left(const Bignum &value, + const std::size_t bits) noexcept -> Bignum { + Bignum result; + const auto word_shift{bits / 64}; + const auto bit_shift{bits % 64}; + result.size = value.size + word_shift + 1; + if (result.size > Bignum::capacity) { + result.size = Bignum::capacity; + } + + for (std::size_t index = 0; index < value.size; ++index) { + const auto destination{index + word_shift}; + if (destination >= Bignum::capacity) { + break; + } + + result.words[destination] |= value.words[index] << bit_shift; + if (bit_shift > 0 && destination + 1 < Bignum::capacity) { + result.words[destination + 1] |= value.words[index] >> (64u - bit_shift); + } + } + + bignum_normalize(result); + return result; +} + +// Assumes the left operand is greater than or equal to the right one +inline auto bignum_subtract_in_place(Bignum &left, const Bignum &right) noexcept + -> void { + std::uint64_t borrow{0}; + for (std::size_t index = 0; index < left.size; ++index) { + const auto subtrahend{index < right.size ? right.words[index] : 0}; + const auto previous{left.words[index]}; + left.words[index] = previous - subtrahend - borrow; + borrow = (previous < subtrahend || (borrow == 1 && previous == subtrahend)) + ? 1 + : 0; + } + + bignum_normalize(left); +} + +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); + } + + bignum_subtract_in_place(value, shifted); + } +} + +// Assumes both operands fit in half the capacity +inline auto bignum_multiply(const Bignum &left, const Bignum &right) noexcept + -> Bignum { + Bignum result; + result.size = left.size + right.size; + if (result.size > Bignum::capacity) { + result.size = Bignum::capacity; + } + + for (std::size_t left_index = 0; left_index < left.size; ++left_index) { + std::uint64_t carry{0}; + for (std::size_t right_index = 0; right_index < right.size; ++right_index) { + const auto destination{left_index + right_index}; + if (destination >= Bignum::capacity) { + break; + } + + const auto product{static_cast(left.words[left_index]) * + right.words[right_index] + + result.words[destination] + carry}; + result.words[destination] = static_cast(product); + carry = static_cast(product >> 64u); + } + + const auto carry_destination{left_index + right.size}; + if (carry_destination < Bignum::capacity) { + result.words[carry_destination] += carry; + } + } + + bignum_normalize(result); + return result; +} + +inline auto bignum_mod_exp(const Bignum &base, const Bignum &exponent, + const Bignum &modulus) noexcept -> Bignum { + Bignum result; + result.words[0] = 1; + result.size = 1; + + auto reduced_base{base}; + bignum_reduce(reduced_base, modulus); + + const auto exponent_bits{bignum_bit_length(exponent)}; + for (std::size_t index = exponent_bits; index > 0; --index) { + result = bignum_multiply(result, result); + bignum_reduce(result, modulus); + if (bignum_get_bit(exponent, index - 1)) { + result = bignum_multiply(result, reduced_base); + bignum_reduce(result, modulus); + } + } + + return result; +} + +inline auto bignum_add(const Bignum &left, const Bignum &right) noexcept + -> Bignum { + Bignum result; + const auto larger{left.size > right.size ? left.size : right.size}; + std::uint64_t carry{0}; + for (std::size_t index = 0; index < larger; ++index) { + const auto first{index < left.size ? left.words[index] : 0}; + const auto second{index < right.size ? right.words[index] : 0}; + const auto sum{static_cast(first) + second + carry}; + result.words[index] = static_cast(sum); + carry = static_cast(sum >> 64u); + } + + result.size = larger; + if (carry > 0 && larger < Bignum::capacity) { + result.words[larger] = carry; + result.size = larger + 1; + } + + bignum_normalize(result); + return result; +} + +inline auto bignum_shift_right(const Bignum &value, + const std::size_t bits) noexcept -> Bignum { + Bignum result; + const auto word_shift{bits / 64}; + const auto bit_shift{bits % 64}; + if (word_shift >= value.size) { + return result; + } + + result.size = value.size - word_shift; + for (std::size_t index = 0; index < result.size; ++index) { + auto word{value.words[index + word_shift] >> bit_shift}; + if (bit_shift > 0 && index + word_shift + 1 < value.size) { + word |= value.words[index + word_shift + 1] << (64u - bit_shift); + } + + result.words[index] = word; + } + + bignum_normalize(result); + return result; +} + +// All modular helpers below assume their operands are already reduced to +// less than the modulus, as the elliptic curve routines guarantee + +inline auto bignum_mod_add(const Bignum &left, const Bignum &right, + const Bignum &modulus) noexcept -> Bignum { + auto result{bignum_add(left, right)}; + if (bignum_compare(result, modulus) >= 0) { + bignum_subtract_in_place(result, modulus); + } + + return result; +} + +inline auto bignum_mod_subtract(const Bignum &left, const Bignum &right, + const Bignum &modulus) noexcept -> Bignum { + if (bignum_compare(left, right) >= 0) { + auto result{left}; + bignum_subtract_in_place(result, right); + return result; + } + + auto result{bignum_add(left, modulus)}; + bignum_subtract_in_place(result, right); + return result; +} + +inline auto bignum_mod_multiply(const Bignum &left, const Bignum &right, + const Bignum &modulus) noexcept -> Bignum { + auto result{bignum_multiply(left, right)}; + bignum_reduce(result, modulus); + return result; +} + +// The modulus must be prime, so that Fermat's little theorem gives the +// inverse as the modulus-minus-two power +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); +} + +inline auto bignum_to_bytes(const Bignum &value, const std::size_t length) + -> std::string { + std::string result(length, '\x00'); + for (std::size_t index = 0; index < length; ++index) { + const auto word_index{index / 8}; + if (word_index >= value.size) { + break; + } + + const auto byte{static_cast( + (value.words[word_index] >> (8 * (index % 8))) & 0xffu)}; + result[length - 1 - index] = static_cast(byte); + } + + return result; +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_ecc.h b/vendor/core/src/core/crypto/crypto_ecc.h new file mode 100644 index 00000000..04f53d98 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_ecc.h @@ -0,0 +1,206 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_ECC_H_ +#define SOURCEMETA_CORE_CRYPTO_ECC_H_ + +// 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 + +#include "crypto_bignum.h" + +#include // std::size_t +#include // std::string_view + +namespace sourcemeta::core { + +struct EllipticCurveParameters { + Bignum prime; + Bignum coefficient_a; + Bignum coefficient_b; + Bignum generator_x; + Bignum generator_y; + Bignum order; + std::size_t field_bytes; +}; + +// A point in Jacobian coordinates, where the affine point is +// (X / Z^2, Y / Z^3). A zero Z marks the point at infinity +struct JacobianPoint { + Bignum x; + Bignum y; + Bignum z; +}; + +// 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}; +} + +inline auto curve_p384() -> EllipticCurveParameters { + // The hexadecimal constants are single string literals so that no digit + // 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}; + // 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}; + // clang-format on +} + +inline auto point_is_infinity(const JacobianPoint &point) noexcept -> bool { + return bignum_is_zero(point.z); +} + +// Point doubling in Jacobian coordinates (the general short Weierstrass +// formulas, which hold for the NIST curves where the coefficient is -3) +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{}}; + } + + 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 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)}; + 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}; +} + +// Point addition in Jacobian coordinates +inline auto point_add(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 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)}; + + if (bignum_compare(u1, u2) == 0) { + if (bignum_compare(s1, s2) != 0) { + return {Bignum{}, Bignum{}, Bignum{}}; + } + + return point_double(left, curve); + } + + 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 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)}; + 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}; +} + +inline auto point_scalar_multiply(const Bignum &scalar, + const JacobianPoint &point, + const EllipticCurveParameters &curve) + -> JacobianPoint { + JacobianPoint result{Bignum{}, Bignum{}, Bignum{}}; + const auto bits{bignum_bit_length(scalar)}; + 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); + } + } + + return result; +} + +// Recover the affine x coordinate (X / Z^2) of a Jacobian point +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); +} + +// 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 right{bignum_mod_add( + bignum_mod_add(x_cubed, + bignum_mod_multiply(curve.coefficient_a, x, prime), prime), + curve.coefficient_b, prime)}; + return bignum_compare(left, right) == 0; +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_fnv128.cc b/vendor/core/src/core/crypto/crypto_fnv128.cc index e5d643ca..8ce5a30e 100644 --- a/vendor/core/src/core/crypto/crypto_fnv128.cc +++ b/vendor/core/src/core/crypto/crypto_fnv128.cc @@ -1,14 +1,11 @@ #include +#include #include // std::array #include // std::uint8_t, std::uint32_t, std::uint64_t namespace { -constexpr std::array HEX_DIGITS{{'0', '1', '2', '3', '4', '5', '6', - '7', '8', '9', 'a', 'b', 'c', 'd', - 'e', 'f', '\0'}}; - // The 128-bit FNV offset basis, in two 64-bit limbs // (draft-eastlake-fnv Section 5) constexpr std::uint64_t OFFSET_BASIS_HIGH{0x6c62272e07bb0142ULL}; @@ -68,14 +65,8 @@ auto fnv128_digest(const std::string_view input) auto fnv128(const std::string_view input) -> std::string { const auto digest = fnv128_digest(input); - std::string result; - result.reserve(32); - for (std::uint64_t index = 0u; index < 16u; ++index) { - result.push_back(HEX_DIGITS[(digest[index] >> 4u) & 0x0fu]); - result.push_back(HEX_DIGITS[digest[index] & 0x0fu]); - } - - return result; + return bytes_to_hex( + {reinterpret_cast(digest.data()), digest.size()}); } auto fnv128(const std::string_view input, std::ostream &output) -> void { diff --git a/vendor/core/src/core/crypto/crypto_helpers.h b/vendor/core/src/core/crypto/crypto_helpers.h new file mode 100644 index 00000000..645b4d96 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_helpers.h @@ -0,0 +1,90 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_HELPERS_H_ +#define SOURCEMETA_CORE_CRYPTO_HELPERS_H_ + +#include +#include +#include +#include + +#include // std::size_t +#include // std::string +#include // std::string_view +#include // std::unreachable + +namespace sourcemeta::core { + +// The largest RSA key any backend accepts, so that every backend agrees on +// the range of valid key sizes +inline constexpr std::size_t MAXIMUM_KEY_BYTES{512}; + +inline auto curve_field_bytes(const EllipticCurve curve) noexcept + -> std::size_t { + switch (curve) { + case EllipticCurve::P256: + return 32; + case EllipticCurve::P384: + return 48; + case EllipticCurve::P521: + return 66; + } + + std::unreachable(); +} + +inline auto digest_message(const SignatureHashFunction hash, + const std::string_view message) -> std::string { + switch (hash) { + case SignatureHashFunction::SHA256: { + const auto digest{sha256_digest(message)}; + return {reinterpret_cast(digest.data()), digest.size()}; + } + case SignatureHashFunction::SHA384: { + const auto digest{sha384_digest(message)}; + return {reinterpret_cast(digest.data()), digest.size()}; + } + case SignatureHashFunction::SHA512: { + const auto digest{sha512_digest(message)}; + return {reinterpret_cast(digest.data()), digest.size()}; + } + } + + std::unreachable(); +} + +inline auto der_append_length(std::string &output, const std::size_t length) + -> void { + if (length < 128) { + output.push_back(static_cast(length)); + } else if (length < 256) { + output.push_back('\x81'); + output.push_back(static_cast(length)); + } else { + output.push_back('\x82'); + output.push_back(static_cast((length >> 8u) & 0xffu)); + output.push_back(static_cast(length & 0xffu)); + } +} + +inline auto der_append_unsigned_integer(std::string &output, + std::string_view value) -> void { + while (!value.empty() && value.front() == '\x00') { + value.remove_prefix(1); + } + + // A leading zero byte keeps the value positive when its high bit is set, + // and represents the value zero when nothing remains + const auto needs_zero_prefix{ + value.empty() || + (static_cast(value.front()) & 0x80u) != 0}; + output.push_back('\x02'); + der_append_length(output, value.size() + (needs_zero_prefix ? 1 : 0)); + if (needs_zero_prefix) { + output.push_back('\x00'); + } + + output.append(value); +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_random.h b/vendor/core/src/core/crypto/crypto_random.h new file mode 100644 index 00000000..a6a220c6 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_random.h @@ -0,0 +1,14 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_RANDOM_H_ +#define SOURCEMETA_CORE_CRYPTO_RANDOM_H_ + +#include // std::array + +namespace sourcemeta::core { + +// Fill the given buffer with random bytes from the system provider where +// available. Defined once per backend +auto fill_random_bytes(std::array &bytes) -> void; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_random_apple.cc b/vendor/core/src/core/crypto/crypto_random_apple.cc new file mode 100644 index 00000000..aafa7f6a --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_random_apple.cc @@ -0,0 +1,19 @@ +#include "crypto_random.h" + +#include // errSecSuccess +#include // SecRandomCopyBytes, kSecRandomDefault + +#include // std::array +#include // std::runtime_error + +namespace sourcemeta::core { + +auto fill_random_bytes(std::array &bytes) -> void { + if (SecRandomCopyBytes(kSecRandomDefault, bytes.size(), bytes.data()) != + errSecSuccess) { + throw std::runtime_error( + "Could not generate random bytes with the Security framework"); + } +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_random_openssl.cc b/vendor/core/src/core/crypto/crypto_random_openssl.cc new file mode 100644 index 00000000..ac4c6325 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_random_openssl.cc @@ -0,0 +1,16 @@ +#include "crypto_random.h" + +#include // RAND_bytes + +#include // std::array +#include // std::runtime_error + +namespace sourcemeta::core { + +auto fill_random_bytes(std::array &bytes) -> void { + if (RAND_bytes(bytes.data(), static_cast(bytes.size())) != 1) { + throw std::runtime_error("Could not generate random bytes with OpenSSL"); + } +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_random_other.cc b/vendor/core/src/core/crypto/crypto_random_other.cc new file mode 100644 index 00000000..72e2a83c --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_random_other.cc @@ -0,0 +1,19 @@ +#include "crypto_random.h" + +#include // std::array +#include // std::random_device, std::mt19937, std::uniform_int_distribution + +namespace sourcemeta::core { + +auto fill_random_bytes(std::array &bytes) -> void { + // Not a cryptographically secure generator. This fallback only exists to + // keep the module buildable on platforms without a system provider + thread_local std::random_device device; + thread_local std::mt19937 generator{device()}; + std::uniform_int_distribution distribution{0, 255}; + for (auto &byte : bytes) { + byte = static_cast(distribution(generator)); + } +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_random_windows.cc b/vendor/core/src/core/crypto/crypto_random_windows.cc new file mode 100644 index 00000000..695da990 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_random_windows.cc @@ -0,0 +1,22 @@ +#include "crypto_random.h" + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG + +#include // BCrypt*, BCRYPT_* + +#include // std::array +#include // std::runtime_error + +namespace sourcemeta::core { + +auto fill_random_bytes(std::array &bytes) -> void { + if (!BCRYPT_SUCCESS(BCryptGenRandom(nullptr, bytes.data(), + static_cast(bytes.size()), + BCRYPT_USE_SYSTEM_PREFERRED_RNG))) { + throw std::runtime_error("Could not generate random bytes with CNG"); + } +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha1.cc b/vendor/core/src/core/crypto/crypto_sha1.cc index 33dccfad..b3f4a9ae 100644 --- a/vendor/core/src/core/crypto/crypto_sha1.cc +++ b/vendor/core/src/core/crypto/crypto_sha1.cc @@ -1,223 +1,14 @@ #include -#include // std::array -#include // std::uint32_t, std::uint64_t - -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL -#include // EVP_MD_CTX_new, EVP_DigestInit_ex, EVP_sha1, EVP_DigestUpdate, EVP_DigestFinal_ex, EVP_MD_CTX_free -#include // std::runtime_error -#else -#include // std::memcpy -#endif - -namespace { -constexpr std::array HEX_DIGITS{{'0', '1', '2', '3', '4', '5', '6', - '7', '8', '9', 'a', 'b', 'c', 'd', - 'e', 'f', '\0'}}; -} // namespace - -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL - -namespace sourcemeta::core { - -auto sha1(const std::string_view input) -> std::string { - auto *context = EVP_MD_CTX_new(); - if (context == nullptr) { - throw std::runtime_error("Could not allocate OpenSSL digest context"); - } - - if (EVP_DigestInit_ex(context, EVP_sha1(), nullptr) != 1 || - EVP_DigestUpdate(context, input.data(), input.size()) != 1) { - EVP_MD_CTX_free(context); - throw std::runtime_error("Could not compute SHA-1 digest"); - } - - std::array digest{}; - unsigned int length = 0; - if (EVP_DigestFinal_ex(context, digest.data(), &length) != 1) { - EVP_MD_CTX_free(context); - throw std::runtime_error("Could not finalize SHA-1 digest"); - } - - EVP_MD_CTX_free(context); - - std::string result; - result.reserve(40); - for (std::uint64_t index = 0; index < 20u; ++index) { - result.push_back(HEX_DIGITS[(digest[index] >> 4u) & 0x0fu]); - result.push_back(HEX_DIGITS[digest[index] & 0x0fu]); - } - - return result; -} - -auto sha1(const std::string_view input, std::ostream &output) -> void { - const auto result = sha1(input); - output.write(result.data(), static_cast(result.size())); -} - -} // namespace sourcemeta::core - -#else - -namespace { - -inline constexpr auto rotate_left(std::uint32_t value, - std::uint64_t count) noexcept - -> std::uint32_t { - return (value << count) | (value >> (32u - count)); -} - -// Equivalent to (x & y) ^ (~x & z) but avoids a bitwise NOT -// (RFC 3174 Section 5, rounds 0 to 19) -inline constexpr auto choice(std::uint32_t x, std::uint32_t y, - std::uint32_t z) noexcept -> std::uint32_t { - return z ^ (x & (y ^ z)); -} - -// RFC 3174 Section 5, rounds 20 to 39 and 60 to 79 -inline constexpr auto parity(std::uint32_t x, std::uint32_t y, - std::uint32_t z) noexcept -> std::uint32_t { - return x ^ y ^ z; -} - -// RFC 3174 Section 5, rounds 40 to 59 -inline constexpr auto majority(std::uint32_t x, std::uint32_t y, - std::uint32_t z) noexcept -> std::uint32_t { - return (x & y) ^ (x & z) ^ (y & z); -} - -inline auto sha1_process_block(const unsigned char *block, - std::array &state) noexcept - -> void { - // Decode 16 big-endian 32-bit words from the block - std::array schedule; - for (std::uint64_t word_index = 0; word_index < 16u; ++word_index) { - const std::uint64_t byte_index = word_index * 4u; - schedule[word_index] = - (static_cast(block[byte_index]) << 24u) | - (static_cast(block[byte_index + 1u]) << 16u) | - (static_cast(block[byte_index + 2u]) << 8u) | - static_cast(block[byte_index + 3u]); - } - - // Extend the message schedule (RFC 3174 Section 6.1 step b) - for (std::uint64_t index = 16u; index < 80u; ++index) { - schedule[index] = - rotate_left(schedule[index - 3u] ^ schedule[index - 8u] ^ - schedule[index - 14u] ^ schedule[index - 16u], - 1u); - } - - auto working = state; - - // Compression function (RFC 3174 Section 6.1 step d), with the round - // constants of RFC 3174 Section 5 - for (std::uint64_t round_index = 0u; round_index < 80u; ++round_index) { - std::uint32_t function_value; - std::uint32_t round_constant; - if (round_index < 20u) { - function_value = choice(working[1], working[2], working[3]); - round_constant = 0x5a827999U; - } else if (round_index < 40u) { - function_value = parity(working[1], working[2], working[3]); - round_constant = 0x6ed9eba1U; - } else if (round_index < 60u) { - function_value = majority(working[1], working[2], working[3]); - round_constant = 0x8f1bbcdcU; - } else { - function_value = parity(working[1], working[2], working[3]); - round_constant = 0xca62c1d6U; - } - - const auto temporary = rotate_left(working[0], 5u) + function_value + - working[4] + schedule[round_index] + round_constant; - - working[4] = working[3]; - working[3] = working[2]; - working[2] = rotate_left(working[1], 30u); - working[1] = working[0]; - working[0] = temporary; - } - - for (std::uint64_t index = 0u; index < 5u; ++index) { - state[index] += working[index]; - } -} - -} // namespace +#include // std::ostream, std::streamsize +#include // std::string +#include // std::string_view namespace sourcemeta::core { -auto sha1(const std::string_view input) -> std::string { - // Initial hash values (RFC 3174 Section 6.1) - std::array state{}; - state[0] = 0x67452301U; - state[1] = 0xefcdab89U; - state[2] = 0x98badcfeU; - state[3] = 0x10325476U; - state[4] = 0xc3d2e1f0U; - - const auto *const input_bytes = - reinterpret_cast(input.data()); - const std::size_t input_length = input.size(); - - // Process all full 64-byte blocks directly from the input (streaming) - std::size_t processed_bytes = 0u; - while (input_length - processed_bytes >= 64u) { - sha1_process_block(input_bytes + processed_bytes, state); - processed_bytes += 64u; - } - - // Prepare the final block(s) (one or two 64-byte blocks) - std::array final_block{}; - const std::size_t remaining_bytes = input_length - processed_bytes; - if (remaining_bytes > 0u) { - std::memcpy(final_block.data(), input_bytes + processed_bytes, - remaining_bytes); - } - - // Append the 0x80 byte after the message data (RFC 3174 Section 4) - final_block[remaining_bytes] = 0x80u; - - // Append length in bits as big-endian 64-bit at the end of the padding - const std::uint64_t message_length_bits = - static_cast(input_length) * 8ull; - - if (remaining_bytes < 56u) { - for (std::uint64_t index = 0u; index < 8u; ++index) { - final_block[56u + index] = static_cast( - (message_length_bits >> (8u * (7u - index))) & 0xffu); - } - sha1_process_block(final_block.data(), state); - } else { - for (std::uint64_t index = 0u; index < 8u; ++index) { - final_block[64u + 56u + index] = static_cast( - (message_length_bits >> (8u * (7u - index))) & 0xffu); - } - - sha1_process_block(final_block.data(), state); - sha1_process_block(final_block.data() + 64u, state); - } - - std::string result; - result.reserve(40); - for (std::uint64_t state_index = 0u; state_index < 5u; ++state_index) { - const auto value = state[state_index]; - for (std::uint64_t nibble = 0u; nibble < 8u; ++nibble) { - const auto shift = 28u - nibble * 4u; - result.push_back(HEX_DIGITS[(value >> shift) & 0x0fu]); - } - } - - return result; -} - auto sha1(const std::string_view input, std::ostream &output) -> void { const auto result = sha1(input); output.write(result.data(), static_cast(result.size())); } } // namespace sourcemeta::core - -#endif diff --git a/vendor/core/src/core/crypto/crypto_sha1_apple.cc b/vendor/core/src/core/crypto/crypto_sha1_apple.cc new file mode 100644 index 00000000..7a47a2c6 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha1_apple.cc @@ -0,0 +1,42 @@ +#include +#include + +#include // CC_SHA1*, CC_LONG + +#include // std::array +#include // std::size_t +#include // std::numeric_limits + +namespace sourcemeta::core { + +auto sha1(const std::string_view input) -> std::string { + // The platform marks its SHA-1 interfaces as deprecated because the + // algorithm is cryptographically broken, but this module keeps exposing + // SHA-1 for non-security use cases +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + CC_SHA1_CTX context; + CC_SHA1_Init(&context); + + // The platform update interface takes a 32-bit length, so larger + // inputs must be fed in chunks + const auto *remaining_data{input.data()}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + while (remaining_size > 0) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + CC_SHA1_Update(&context, remaining_data, static_cast(chunk_size)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + CC_SHA1_Final(digest.data(), &context); +#pragma clang diagnostic pop + + return bytes_to_hex( + {reinterpret_cast(digest.data()), digest.size()}); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha1_openssl.cc b/vendor/core/src/core/crypto/crypto_sha1_openssl.cc new file mode 100644 index 00000000..576655eb --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha1_openssl.cc @@ -0,0 +1,36 @@ +#include +#include + +#include // EVP_* + +#include // std::array +#include // std::runtime_error + +namespace sourcemeta::core { + +auto sha1(const std::string_view input) -> std::string { + auto *context = EVP_MD_CTX_new(); + if (context == nullptr) { + throw std::runtime_error("Could not allocate OpenSSL digest context"); + } + + if (EVP_DigestInit_ex(context, EVP_sha1(), nullptr) != 1 || + EVP_DigestUpdate(context, input.data(), input.size()) != 1) { + EVP_MD_CTX_free(context); + throw std::runtime_error("Could not compute SHA-1 digest"); + } + + std::array digest{}; + unsigned int length = 0; + if (EVP_DigestFinal_ex(context, digest.data(), &length) != 1) { + EVP_MD_CTX_free(context); + throw std::runtime_error("Could not finalize SHA-1 digest"); + } + + EVP_MD_CTX_free(context); + + return bytes_to_hex( + {reinterpret_cast(digest.data()), digest.size()}); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha1_other.cc b/vendor/core/src/core/crypto/crypto_sha1_other.cc new file mode 100644 index 00000000..89fc9062 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha1_other.cc @@ -0,0 +1,161 @@ +#include +#include + +#include // std::array +#include // std::size_t +#include // std::uint32_t, std::uint64_t +#include // std::memcpy + +namespace { + +inline constexpr auto rotate_left(std::uint32_t value, + std::uint64_t count) noexcept + -> std::uint32_t { + return (value << count) | (value >> (32u - count)); +} + +// Equivalent to (x & y) ^ (~x & z) but avoids a bitwise NOT +// (RFC 3174 Section 5, rounds 0 to 19) +inline constexpr auto choice(std::uint32_t x, std::uint32_t y, + std::uint32_t z) noexcept -> std::uint32_t { + return z ^ (x & (y ^ z)); +} + +// RFC 3174 Section 5, rounds 20 to 39 and 60 to 79 +inline constexpr auto parity(std::uint32_t x, std::uint32_t y, + std::uint32_t z) noexcept -> std::uint32_t { + return x ^ y ^ z; +} + +// RFC 3174 Section 5, rounds 40 to 59 +inline constexpr auto majority(std::uint32_t x, std::uint32_t y, + std::uint32_t z) noexcept -> std::uint32_t { + return (x & y) ^ (x & z) ^ (y & z); +} + +inline auto sha1_process_block(const unsigned char *block, + std::array &state) noexcept + -> void { + // Decode 16 big-endian 32-bit words from the block + std::array schedule; + for (std::uint64_t word_index = 0; word_index < 16u; ++word_index) { + const std::uint64_t byte_index = word_index * 4u; + schedule[word_index] = + (static_cast(block[byte_index]) << 24u) | + (static_cast(block[byte_index + 1u]) << 16u) | + (static_cast(block[byte_index + 2u]) << 8u) | + static_cast(block[byte_index + 3u]); + } + + // Extend the message schedule (RFC 3174 Section 6.1 step b) + for (std::uint64_t index = 16u; index < 80u; ++index) { + schedule[index] = + rotate_left(schedule[index - 3u] ^ schedule[index - 8u] ^ + schedule[index - 14u] ^ schedule[index - 16u], + 1u); + } + + auto working = state; + + // Compression function (RFC 3174 Section 6.1 step d), with the round + // constants of RFC 3174 Section 5 + for (std::uint64_t round_index = 0u; round_index < 80u; ++round_index) { + std::uint32_t function_value; + std::uint32_t round_constant; + if (round_index < 20u) { + function_value = choice(working[1], working[2], working[3]); + round_constant = 0x5a827999U; + } else if (round_index < 40u) { + function_value = parity(working[1], working[2], working[3]); + round_constant = 0x6ed9eba1U; + } else if (round_index < 60u) { + function_value = majority(working[1], working[2], working[3]); + round_constant = 0x8f1bbcdcU; + } else { + function_value = parity(working[1], working[2], working[3]); + round_constant = 0xca62c1d6U; + } + + const auto temporary = rotate_left(working[0], 5u) + function_value + + working[4] + schedule[round_index] + round_constant; + + working[4] = working[3]; + working[3] = working[2]; + working[2] = rotate_left(working[1], 30u); + working[1] = working[0]; + working[0] = temporary; + } + + for (std::uint64_t index = 0u; index < 5u; ++index) { + state[index] += working[index]; + } +} + +} // namespace + +namespace sourcemeta::core { + +auto sha1(const std::string_view input) -> std::string { + // Initial hash values (RFC 3174 Section 6.1) + std::array state{}; + state[0] = 0x67452301U; + state[1] = 0xefcdab89U; + state[2] = 0x98badcfeU; + state[3] = 0x10325476U; + state[4] = 0xc3d2e1f0U; + + const auto *const input_bytes = + reinterpret_cast(input.data()); + const std::size_t input_length = input.size(); + + // Process all full 64-byte blocks directly from the input (streaming) + std::size_t processed_bytes = 0u; + while (input_length - processed_bytes >= 64u) { + sha1_process_block(input_bytes + processed_bytes, state); + processed_bytes += 64u; + } + + // Prepare the final block(s) (one or two 64-byte blocks) + std::array final_block{}; + const std::size_t remaining_bytes = input_length - processed_bytes; + if (remaining_bytes > 0u) { + std::memcpy(final_block.data(), input_bytes + processed_bytes, + remaining_bytes); + } + + // Append the 0x80 byte after the message data (RFC 3174 Section 4) + final_block[remaining_bytes] = 0x80u; + + // Append length in bits as big-endian 64-bit at the end of the padding + const std::uint64_t message_length_bits = + static_cast(input_length) * 8ull; + + if (remaining_bytes < 56u) { + for (std::uint64_t index = 0u; index < 8u; ++index) { + final_block[56u + index] = static_cast( + (message_length_bits >> (8u * (7u - index))) & 0xffu); + } + sha1_process_block(final_block.data(), state); + } else { + for (std::uint64_t index = 0u; index < 8u; ++index) { + final_block[64u + 56u + index] = static_cast( + (message_length_bits >> (8u * (7u - index))) & 0xffu); + } + + sha1_process_block(final_block.data(), state); + sha1_process_block(final_block.data() + 64u, state); + } + + std::array digest{}; + for (std::uint64_t state_index = 0u; state_index < 5u; ++state_index) { + for (std::uint64_t byte_index = 0u; byte_index < 4u; ++byte_index) { + digest[(state_index * 4u) + byte_index] = static_cast( + (state[state_index] >> (8u * (3u - byte_index))) & 0xffu); + } + } + + return bytes_to_hex( + {reinterpret_cast(digest.data()), digest.size()}); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha1_windows.cc b/vendor/core/src/core/crypto/crypto_sha1_windows.cc new file mode 100644 index 00000000..c83793a0 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha1_windows.cc @@ -0,0 +1,65 @@ +#include +#include + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG + +#include // BCrypt*, BCRYPT_* + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits +#include // std::runtime_error + +namespace sourcemeta::core { + +auto sha1(const std::string_view input) -> std::string { + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, BCRYPT_SHA1_ALGORITHM, nullptr, 0))) { + throw std::runtime_error("Could not open the CNG SHA-1 provider"); + } + + BCRYPT_HASH_HANDLE hash{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + throw std::runtime_error("Could not create the CNG SHA-1 hash"); + } + + // The data interface is not const-qualified but never writes through + // the pointer, and it takes a 32-bit length, so larger inputs must be + // fed in chunks + auto *remaining_data{ + reinterpret_cast(const_cast(input.data()))}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + auto success{true}; + while (remaining_size > 0 && success) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + success = BCRYPT_SUCCESS(BCryptHashData(hash, remaining_data, + static_cast(chunk_size), 0)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + if (success) { + success = BCRYPT_SUCCESS(BCryptFinishHash( + hash, digest.data(), static_cast(digest.size()), 0)); + } + + BCryptDestroyHash(hash); + BCryptCloseAlgorithmProvider(algorithm, 0); + if (!success) { + throw std::runtime_error("Could not compute the CNG SHA-1 digest"); + } + + return bytes_to_hex( + {reinterpret_cast(digest.data()), digest.size()}); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha256.cc b/vendor/core/src/core/crypto/crypto_sha256.cc index 0d830da6..3397473f 100644 --- a/vendor/core/src/core/crypto/crypto_sha256.cc +++ b/vendor/core/src/core/crypto/crypto_sha256.cc @@ -1,54 +1,16 @@ #include +#include -#include // std::array -#include // std::uint32_t, std::uint64_t - -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL -#include // EVP_MD_CTX_new, EVP_DigestInit_ex, EVP_sha256, EVP_DigestUpdate, EVP_DigestFinal_ex, EVP_MD_CTX_free -#include // std::runtime_error -#else -#include // std::memcpy -#endif - -namespace { -constexpr std::array HEX_DIGITS{{'0', '1', '2', '3', '4', '5', '6', - '7', '8', '9', 'a', 'b', 'c', 'd', - 'e', 'f', '\0'}}; -} // namespace - -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL +#include // std::ostream, std::streamsize +#include // std::string +#include // std::string_view namespace sourcemeta::core { auto sha256(const std::string_view input) -> std::string { - auto *context = EVP_MD_CTX_new(); - if (context == nullptr) { - throw std::runtime_error("Could not allocate OpenSSL digest context"); - } - - if (EVP_DigestInit_ex(context, EVP_sha256(), nullptr) != 1 || - EVP_DigestUpdate(context, input.data(), input.size()) != 1) { - EVP_MD_CTX_free(context); - throw std::runtime_error("Could not compute SHA-256 digest"); - } - - std::array digest{}; - unsigned int length = 0; - if (EVP_DigestFinal_ex(context, digest.data(), &length) != 1) { - EVP_MD_CTX_free(context); - throw std::runtime_error("Could not finalize SHA-256 digest"); - } - - EVP_MD_CTX_free(context); - - std::string result; - result.reserve(64); - for (std::uint64_t index = 0; index < 32u; ++index) { - result.push_back(HEX_DIGITS[(digest[index] >> 4u) & 0x0fu]); - result.push_back(HEX_DIGITS[digest[index] & 0x0fu]); - } - - return result; + const auto digest{sha256_digest(input)}; + return bytes_to_hex( + {reinterpret_cast(digest.data()), digest.size()}); } auto sha256(const std::string_view input, std::ostream &output) -> void { @@ -57,192 +19,3 @@ auto sha256(const std::string_view input, std::ostream &output) -> void { } } // namespace sourcemeta::core - -#else - -namespace { - -inline constexpr auto rotate_right(std::uint32_t value, - std::uint64_t count) noexcept - -> std::uint32_t { - return (value >> count) | (value << (32u - count)); -} - -// FIPS 180-4 Section 4.1.2 logical functions -inline constexpr auto big_sigma_0(std::uint32_t value) noexcept - -> std::uint32_t { - return rotate_right(value, 2u) ^ rotate_right(value, 13u) ^ - rotate_right(value, 22u); -} - -inline constexpr auto big_sigma_1(std::uint32_t value) noexcept - -> std::uint32_t { - return rotate_right(value, 6u) ^ rotate_right(value, 11u) ^ - rotate_right(value, 25u); -} - -inline constexpr auto small_sigma_0(std::uint32_t value) noexcept - -> std::uint32_t { - return rotate_right(value, 7u) ^ rotate_right(value, 18u) ^ (value >> 3u); -} - -inline constexpr auto small_sigma_1(std::uint32_t value) noexcept - -> std::uint32_t { - return rotate_right(value, 17u) ^ rotate_right(value, 19u) ^ (value >> 10u); -} - -// Equivalent to (x & y) ^ (~x & z) but avoids a bitwise NOT -inline constexpr auto choice(std::uint32_t x, std::uint32_t y, - std::uint32_t z) noexcept -> std::uint32_t { - return z ^ (x & (y ^ z)); -} - -inline constexpr auto majority(std::uint32_t x, std::uint32_t y, - std::uint32_t z) noexcept -> std::uint32_t { - return (x & y) ^ (x & z) ^ (y & z); -} - -inline auto sha256_process_block(const unsigned char *block, - std::array &state) noexcept - -> void { - // First 32 bits of the fractional parts of the cube roots - // of the first 64 prime numbers (FIPS 180-4 Section 4.2.2) - static constexpr std::array round_constants = { - {0x428a2f98U, 0x71374491U, 0xb5c0fbcfU, 0xe9b5dba5U, 0x3956c25bU, - 0x59f111f1U, 0x923f82a4U, 0xab1c5ed5U, 0xd807aa98U, 0x12835b01U, - 0x243185beU, 0x550c7dc3U, 0x72be5d74U, 0x80deb1feU, 0x9bdc06a7U, - 0xc19bf174U, 0xe49b69c1U, 0xefbe4786U, 0x0fc19dc6U, 0x240ca1ccU, - 0x2de92c6fU, 0x4a7484aaU, 0x5cb0a9dcU, 0x76f988daU, 0x983e5152U, - 0xa831c66dU, 0xb00327c8U, 0xbf597fc7U, 0xc6e00bf3U, 0xd5a79147U, - 0x06ca6351U, 0x14292967U, 0x27b70a85U, 0x2e1b2138U, 0x4d2c6dfcU, - 0x53380d13U, 0x650a7354U, 0x766a0abbU, 0x81c2c92eU, 0x92722c85U, - 0xa2bfe8a1U, 0xa81a664bU, 0xc24b8b70U, 0xc76c51a3U, 0xd192e819U, - 0xd6990624U, 0xf40e3585U, 0x106aa070U, 0x19a4c116U, 0x1e376c08U, - 0x2748774cU, 0x34b0bcb5U, 0x391c0cb3U, 0x4ed8aa4aU, 0x5b9cca4fU, - 0x682e6ff3U, 0x748f82eeU, 0x78a5636fU, 0x84c87814U, 0x8cc70208U, - 0x90befffaU, 0xa4506cebU, 0xbef9a3f7U, 0xc67178f2U}}; - - // Decode 16 big-endian 32-bit words from the block - std::array schedule; - for (std::uint64_t word_index = 0; word_index < 16u; ++word_index) { - const std::uint64_t byte_index = word_index * 4u; - schedule[word_index] = - (static_cast(block[byte_index]) << 24u) | - (static_cast(block[byte_index + 1u]) << 16u) | - (static_cast(block[byte_index + 2u]) << 8u) | - static_cast(block[byte_index + 3u]); - } - - // Extend the message schedule (FIPS 180-4 Section 6.2.2 step 1) - for (std::uint64_t index = 16u; index < 64u; ++index) { - schedule[index] = - small_sigma_1(schedule[index - 2u]) + schedule[index - 7u] + - small_sigma_0(schedule[index - 15u]) + schedule[index - 16u]; - } - - auto working = state; - - // Compression function (FIPS 180-4 Section 6.2.2 step 3) - for (std::uint64_t round_index = 0u; round_index < 64u; ++round_index) { - const auto temporary_1 = working[7] + big_sigma_1(working[4]) + - choice(working[4], working[5], working[6]) + - round_constants[round_index] + - schedule[round_index]; - const auto temporary_2 = - big_sigma_0(working[0]) + majority(working[0], working[1], working[2]); - - working[7] = working[6]; - working[6] = working[5]; - working[5] = working[4]; - working[4] = working[3] + temporary_1; - working[3] = working[2]; - working[2] = working[1]; - working[1] = working[0]; - working[0] = temporary_1 + temporary_2; - } - - for (std::uint64_t index = 0u; index < 8u; ++index) { - state[index] += working[index]; - } -} - -} // namespace - -namespace sourcemeta::core { - -auto sha256(const std::string_view input) -> std::string { - // Initial hash values: first 32 bits of the fractional parts of the - // square roots of the first 8 primes (FIPS 180-4 Section 5.3.3) - std::array state{}; - state[0] = 0x6a09e667U; - state[1] = 0xbb67ae85U; - state[2] = 0x3c6ef372U; - state[3] = 0xa54ff53aU; - state[4] = 0x510e527fU; - state[5] = 0x9b05688cU; - state[6] = 0x1f83d9abU; - state[7] = 0x5be0cd19U; - - const auto *const input_bytes = - reinterpret_cast(input.data()); - const std::size_t input_length = input.size(); - - // Process all full 64-byte blocks directly from the input (streaming) - std::size_t processed_bytes = 0u; - while (input_length - processed_bytes >= 64u) { - sha256_process_block(input_bytes + processed_bytes, state); - processed_bytes += 64u; - } - - // Prepare the final block(s) (one or two 64-byte blocks) - std::array final_block{}; - const std::size_t remaining_bytes = input_length - processed_bytes; - if (remaining_bytes > 0u) { - std::memcpy(final_block.data(), input_bytes + processed_bytes, - remaining_bytes); - } - - // Append the 0x80 byte after the message data - final_block[remaining_bytes] = 0x80u; - - // Append length in bits as big-endian 64-bit at the end of the padding - const std::uint64_t message_length_bits = - static_cast(input_length) * 8ull; - - if (remaining_bytes < 56u) { - for (std::uint64_t index = 0u; index < 8u; ++index) { - final_block[56u + index] = static_cast( - (message_length_bits >> (8u * (7u - index))) & 0xffu); - } - sha256_process_block(final_block.data(), state); - } else { - for (std::uint64_t index = 0u; index < 8u; ++index) { - final_block[64u + 56u + index] = static_cast( - (message_length_bits >> (8u * (7u - index))) & 0xffu); - } - - sha256_process_block(final_block.data(), state); - sha256_process_block(final_block.data() + 64u, state); - } - - std::string result; - result.reserve(64); - for (std::uint64_t state_index = 0u; state_index < 8u; ++state_index) { - const auto value = state[state_index]; - for (std::uint64_t nibble = 0u; nibble < 8u; ++nibble) { - const auto shift = 28u - nibble * 4u; - result.push_back(HEX_DIGITS[(value >> shift) & 0x0fu]); - } - } - - return result; -} - -auto sha256(const std::string_view input, std::ostream &output) -> void { - const auto result = sha256(input); - output.write(result.data(), static_cast(result.size())); -} - -} // namespace sourcemeta::core - -#endif diff --git a/vendor/core/src/core/crypto/crypto_sha256_apple.cc b/vendor/core/src/core/crypto/crypto_sha256_apple.cc new file mode 100644 index 00000000..2fccfea2 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha256_apple.cc @@ -0,0 +1,36 @@ +#include + +#include // CC_SHA256*, CC_LONG + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits + +namespace sourcemeta::core { + +auto sha256_digest(const std::string_view input) + -> std::array { + CC_SHA256_CTX context; + CC_SHA256_Init(&context); + + // The platform update interface takes a 32-bit length, so larger + // inputs must be fed in chunks + const auto *remaining_data{input.data()}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + while (remaining_size > 0) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + CC_SHA256_Update(&context, remaining_data, + static_cast(chunk_size)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + CC_SHA256_Final(digest.data(), &context); + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha256_openssl.cc b/vendor/core/src/core/crypto/crypto_sha256_openssl.cc new file mode 100644 index 00000000..d4d655a2 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha256_openssl.cc @@ -0,0 +1,35 @@ +#include + +#include // EVP_* + +#include // std::array +#include // std::uint8_t +#include // std::runtime_error + +namespace sourcemeta::core { + +auto sha256_digest(const std::string_view input) + -> std::array { + auto *context = EVP_MD_CTX_new(); + if (context == nullptr) { + throw std::runtime_error("Could not allocate OpenSSL digest context"); + } + + if (EVP_DigestInit_ex(context, EVP_sha256(), nullptr) != 1 || + EVP_DigestUpdate(context, input.data(), input.size()) != 1) { + EVP_MD_CTX_free(context); + throw std::runtime_error("Could not compute SHA-256 digest"); + } + + std::array digest{}; + unsigned int length = 0; + if (EVP_DigestFinal_ex(context, digest.data(), &length) != 1) { + EVP_MD_CTX_free(context); + throw std::runtime_error("Could not finalize SHA-256 digest"); + } + + EVP_MD_CTX_free(context); + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha256_other.cc b/vendor/core/src/core/crypto/crypto_sha256_other.cc new file mode 100644 index 00000000..63f26139 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha256_other.cc @@ -0,0 +1,185 @@ +#include + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint32_t, std::uint64_t +#include // std::memcpy + +namespace { + +inline constexpr auto rotate_right(std::uint32_t value, + std::uint64_t count) noexcept + -> std::uint32_t { + return (value >> count) | (value << (32u - count)); +} + +// FIPS 180-4 Section 4.1.2 logical functions +inline constexpr auto big_sigma_0(std::uint32_t value) noexcept + -> std::uint32_t { + return rotate_right(value, 2u) ^ rotate_right(value, 13u) ^ + rotate_right(value, 22u); +} + +inline constexpr auto big_sigma_1(std::uint32_t value) noexcept + -> std::uint32_t { + return rotate_right(value, 6u) ^ rotate_right(value, 11u) ^ + rotate_right(value, 25u); +} + +inline constexpr auto small_sigma_0(std::uint32_t value) noexcept + -> std::uint32_t { + return rotate_right(value, 7u) ^ rotate_right(value, 18u) ^ (value >> 3u); +} + +inline constexpr auto small_sigma_1(std::uint32_t value) noexcept + -> std::uint32_t { + return rotate_right(value, 17u) ^ rotate_right(value, 19u) ^ (value >> 10u); +} + +// Equivalent to (x & y) ^ (~x & z) but avoids a bitwise NOT +inline constexpr auto choice(std::uint32_t x, std::uint32_t y, + std::uint32_t z) noexcept -> std::uint32_t { + return z ^ (x & (y ^ z)); +} + +inline constexpr auto majority(std::uint32_t x, std::uint32_t y, + std::uint32_t z) noexcept -> std::uint32_t { + return (x & y) ^ (x & z) ^ (y & z); +} + +inline auto sha256_process_block(const unsigned char *block, + std::array &state) noexcept + -> void { + // First 32 bits of the fractional parts of the cube roots + // of the first 64 prime numbers (FIPS 180-4 Section 4.2.2) + static constexpr std::array round_constants = { + {0x428a2f98U, 0x71374491U, 0xb5c0fbcfU, 0xe9b5dba5U, 0x3956c25bU, + 0x59f111f1U, 0x923f82a4U, 0xab1c5ed5U, 0xd807aa98U, 0x12835b01U, + 0x243185beU, 0x550c7dc3U, 0x72be5d74U, 0x80deb1feU, 0x9bdc06a7U, + 0xc19bf174U, 0xe49b69c1U, 0xefbe4786U, 0x0fc19dc6U, 0x240ca1ccU, + 0x2de92c6fU, 0x4a7484aaU, 0x5cb0a9dcU, 0x76f988daU, 0x983e5152U, + 0xa831c66dU, 0xb00327c8U, 0xbf597fc7U, 0xc6e00bf3U, 0xd5a79147U, + 0x06ca6351U, 0x14292967U, 0x27b70a85U, 0x2e1b2138U, 0x4d2c6dfcU, + 0x53380d13U, 0x650a7354U, 0x766a0abbU, 0x81c2c92eU, 0x92722c85U, + 0xa2bfe8a1U, 0xa81a664bU, 0xc24b8b70U, 0xc76c51a3U, 0xd192e819U, + 0xd6990624U, 0xf40e3585U, 0x106aa070U, 0x19a4c116U, 0x1e376c08U, + 0x2748774cU, 0x34b0bcb5U, 0x391c0cb3U, 0x4ed8aa4aU, 0x5b9cca4fU, + 0x682e6ff3U, 0x748f82eeU, 0x78a5636fU, 0x84c87814U, 0x8cc70208U, + 0x90befffaU, 0xa4506cebU, 0xbef9a3f7U, 0xc67178f2U}}; + + // Decode 16 big-endian 32-bit words from the block + std::array schedule; + for (std::uint64_t word_index = 0; word_index < 16u; ++word_index) { + const std::uint64_t byte_index = word_index * 4u; + schedule[word_index] = + (static_cast(block[byte_index]) << 24u) | + (static_cast(block[byte_index + 1u]) << 16u) | + (static_cast(block[byte_index + 2u]) << 8u) | + static_cast(block[byte_index + 3u]); + } + + // Extend the message schedule (FIPS 180-4 Section 6.2.2 step 1) + for (std::uint64_t index = 16u; index < 64u; ++index) { + schedule[index] = + small_sigma_1(schedule[index - 2u]) + schedule[index - 7u] + + small_sigma_0(schedule[index - 15u]) + schedule[index - 16u]; + } + + auto working = state; + + // Compression function (FIPS 180-4 Section 6.2.2 step 3) + for (std::uint64_t round_index = 0u; round_index < 64u; ++round_index) { + const auto temporary_1 = working[7] + big_sigma_1(working[4]) + + choice(working[4], working[5], working[6]) + + round_constants[round_index] + + schedule[round_index]; + const auto temporary_2 = + big_sigma_0(working[0]) + majority(working[0], working[1], working[2]); + + working[7] = working[6]; + working[6] = working[5]; + working[5] = working[4]; + working[4] = working[3] + temporary_1; + working[3] = working[2]; + working[2] = working[1]; + working[1] = working[0]; + working[0] = temporary_1 + temporary_2; + } + + for (std::uint64_t index = 0u; index < 8u; ++index) { + state[index] += working[index]; + } +} + +} // namespace + +namespace sourcemeta::core { + +auto sha256_digest(const std::string_view input) + -> std::array { + // Initial hash values: first 32 bits of the fractional parts of the + // square roots of the first 8 primes (FIPS 180-4 Section 5.3.3) + std::array state{}; + state[0] = 0x6a09e667U; + state[1] = 0xbb67ae85U; + state[2] = 0x3c6ef372U; + state[3] = 0xa54ff53aU; + state[4] = 0x510e527fU; + state[5] = 0x9b05688cU; + state[6] = 0x1f83d9abU; + state[7] = 0x5be0cd19U; + + const auto *const input_bytes = + reinterpret_cast(input.data()); + const std::size_t input_length = input.size(); + + // Process all full 64-byte blocks directly from the input (streaming) + std::size_t processed_bytes = 0u; + while (input_length - processed_bytes >= 64u) { + sha256_process_block(input_bytes + processed_bytes, state); + processed_bytes += 64u; + } + + // Prepare the final block(s) (one or two 64-byte blocks) + std::array final_block{}; + const std::size_t remaining_bytes = input_length - processed_bytes; + if (remaining_bytes > 0u) { + std::memcpy(final_block.data(), input_bytes + processed_bytes, + remaining_bytes); + } + + // Append the 0x80 byte after the message data + final_block[remaining_bytes] = 0x80u; + + // Append length in bits as big-endian 64-bit at the end of the padding + const std::uint64_t message_length_bits = + static_cast(input_length) * 8ull; + + if (remaining_bytes < 56u) { + for (std::uint64_t index = 0u; index < 8u; ++index) { + final_block[56u + index] = static_cast( + (message_length_bits >> (8u * (7u - index))) & 0xffu); + } + sha256_process_block(final_block.data(), state); + } else { + for (std::uint64_t index = 0u; index < 8u; ++index) { + final_block[64u + 56u + index] = static_cast( + (message_length_bits >> (8u * (7u - index))) & 0xffu); + } + + sha256_process_block(final_block.data(), state); + sha256_process_block(final_block.data() + 64u, state); + } + + std::array digest{}; + for (std::size_t word_index = 0u; word_index < 8u; ++word_index) { + for (std::size_t byte_index = 0u; byte_index < 4u; ++byte_index) { + digest[(word_index * 4u) + byte_index] = static_cast( + (state[word_index] >> (8u * (3u - byte_index))) & 0xffu); + } + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha256_windows.cc b/vendor/core/src/core/crypto/crypto_sha256_windows.cc new file mode 100644 index 00000000..e47e9cfd --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha256_windows.cc @@ -0,0 +1,64 @@ +#include + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG + +#include // BCrypt*, BCRYPT_* + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits +#include // std::runtime_error + +namespace sourcemeta::core { + +auto sha256_digest(const std::string_view input) + -> std::array { + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0))) { + throw std::runtime_error("Could not open the CNG SHA-256 provider"); + } + + BCRYPT_HASH_HANDLE hash{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + throw std::runtime_error("Could not create the CNG SHA-256 hash"); + } + + // The data interface is not const-qualified but never writes through + // the pointer, and it takes a 32-bit length, so larger inputs must be + // fed in chunks + auto *remaining_data{ + reinterpret_cast(const_cast(input.data()))}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + auto success{true}; + while (remaining_size > 0 && success) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + success = BCRYPT_SUCCESS(BCryptHashData(hash, remaining_data, + static_cast(chunk_size), 0)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + if (success) { + success = BCRYPT_SUCCESS(BCryptFinishHash( + hash, digest.data(), static_cast(digest.size()), 0)); + } + + BCryptDestroyHash(hash); + BCryptCloseAlgorithmProvider(algorithm, 0); + if (!success) { + throw std::runtime_error("Could not compute the CNG SHA-256 digest"); + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha2_64.h b/vendor/core/src/core/crypto/crypto_sha2_64.h new file mode 100644 index 00000000..78ebff3b --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha2_64.h @@ -0,0 +1,200 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_SHA2_64_H_ +#define SOURCEMETA_CORE_CRYPTO_SHA2_64_H_ + +// Shared FIPS 180-4 core for the hash functions built on 64-bit words, +// used only by the fallback implementations + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint64_t +#include // std::memcpy +#include // std::string_view + +namespace sourcemeta::core { + +// The count must be between 1 and 63, as the complementary shift is +// undefined otherwise +inline constexpr auto sha2_64_rotate_right(std::uint64_t value, + std::uint64_t count) noexcept + -> std::uint64_t { + return (value >> count) | (value << (64u - count)); +} + +// FIPS 180-4 Section 4.1.3 logical functions +inline constexpr auto sha2_64_big_sigma_0(std::uint64_t value) noexcept + -> std::uint64_t { + return sha2_64_rotate_right(value, 28u) ^ sha2_64_rotate_right(value, 34u) ^ + sha2_64_rotate_right(value, 39u); +} + +inline constexpr auto sha2_64_big_sigma_1(std::uint64_t value) noexcept + -> std::uint64_t { + return sha2_64_rotate_right(value, 14u) ^ sha2_64_rotate_right(value, 18u) ^ + sha2_64_rotate_right(value, 41u); +} + +inline constexpr auto sha2_64_small_sigma_0(std::uint64_t value) noexcept + -> std::uint64_t { + return sha2_64_rotate_right(value, 1u) ^ sha2_64_rotate_right(value, 8u) ^ + (value >> 7u); +} + +inline constexpr auto sha2_64_small_sigma_1(std::uint64_t value) noexcept + -> std::uint64_t { + return sha2_64_rotate_right(value, 19u) ^ sha2_64_rotate_right(value, 61u) ^ + (value >> 6u); +} + +// Equivalent to (x & y) ^ (~x & z) but avoids a bitwise NOT +inline constexpr auto sha2_64_choice(std::uint64_t x, std::uint64_t y, + std::uint64_t z) noexcept + -> std::uint64_t { + return z ^ (x & (y ^ z)); +} + +inline constexpr auto sha2_64_majority(std::uint64_t x, std::uint64_t y, + std::uint64_t z) noexcept + -> std::uint64_t { + return (x & y) ^ (x & z) ^ (y & z); +} + +inline auto sha2_64_process_block(const std::uint8_t *block, + std::array &state) noexcept + -> void { + // First 64 bits of the fractional parts of the cube roots + // of the first 80 prime numbers (FIPS 180-4 Section 4.2.3) + static constexpr std::array round_constants = { + {0x428a2f98d728ae22U, 0x7137449123ef65cdU, 0xb5c0fbcfec4d3b2fU, + 0xe9b5dba58189dbbcU, 0x3956c25bf348b538U, 0x59f111f1b605d019U, + 0x923f82a4af194f9bU, 0xab1c5ed5da6d8118U, 0xd807aa98a3030242U, + 0x12835b0145706fbeU, 0x243185be4ee4b28cU, 0x550c7dc3d5ffb4e2U, + 0x72be5d74f27b896fU, 0x80deb1fe3b1696b1U, 0x9bdc06a725c71235U, + 0xc19bf174cf692694U, 0xe49b69c19ef14ad2U, 0xefbe4786384f25e3U, + 0x0fc19dc68b8cd5b5U, 0x240ca1cc77ac9c65U, 0x2de92c6f592b0275U, + 0x4a7484aa6ea6e483U, 0x5cb0a9dcbd41fbd4U, 0x76f988da831153b5U, + 0x983e5152ee66dfabU, 0xa831c66d2db43210U, 0xb00327c898fb213fU, + 0xbf597fc7beef0ee4U, 0xc6e00bf33da88fc2U, 0xd5a79147930aa725U, + 0x06ca6351e003826fU, 0x142929670a0e6e70U, 0x27b70a8546d22ffcU, + 0x2e1b21385c26c926U, 0x4d2c6dfc5ac42aedU, 0x53380d139d95b3dfU, + 0x650a73548baf63deU, 0x766a0abb3c77b2a8U, 0x81c2c92e47edaee6U, + 0x92722c851482353bU, 0xa2bfe8a14cf10364U, 0xa81a664bbc423001U, + 0xc24b8b70d0f89791U, 0xc76c51a30654be30U, 0xd192e819d6ef5218U, + 0xd69906245565a910U, 0xf40e35855771202aU, 0x106aa07032bbd1b8U, + 0x19a4c116b8d2d0c8U, 0x1e376c085141ab53U, 0x2748774cdf8eeb99U, + 0x34b0bcb5e19b48a8U, 0x391c0cb3c5c95a63U, 0x4ed8aa4ae3418acbU, + 0x5b9cca4f7763e373U, 0x682e6ff3d6b2b8a3U, 0x748f82ee5defb2fcU, + 0x78a5636f43172f60U, 0x84c87814a1f0ab72U, 0x8cc702081a6439ecU, + 0x90befffa23631e28U, 0xa4506cebde82bde9U, 0xbef9a3f7b2c67915U, + 0xc67178f2e372532bU, 0xca273eceea26619cU, 0xd186b8c721c0c207U, + 0xeada7dd6cde0eb1eU, 0xf57d4f7fee6ed178U, 0x06f067aa72176fbaU, + 0x0a637dc5a2c898a6U, 0x113f9804bef90daeU, 0x1b710b35131c471bU, + 0x28db77f523047d84U, 0x32caab7b40c72493U, 0x3c9ebe0a15c9bebcU, + 0x431d67c49c100d4cU, 0x4cc5d4becb3e42b6U, 0x597f299cfc657e2aU, + 0x5fcb6fab3ad6faecU, 0x6c44198c4a475817U}}; + + // Decode 16 big-endian 64-bit words from the block + std::array schedule; + for (std::uint64_t word_index = 0; word_index < 16u; ++word_index) { + const std::uint64_t byte_index = word_index * 8u; + schedule[word_index] = + (static_cast(block[byte_index]) << 56u) | + (static_cast(block[byte_index + 1u]) << 48u) | + (static_cast(block[byte_index + 2u]) << 40u) | + (static_cast(block[byte_index + 3u]) << 32u) | + (static_cast(block[byte_index + 4u]) << 24u) | + (static_cast(block[byte_index + 5u]) << 16u) | + (static_cast(block[byte_index + 6u]) << 8u) | + static_cast(block[byte_index + 7u]); + } + + // Extend the message schedule (FIPS 180-4 Section 6.4.2 step 1) + for (std::uint64_t index = 16u; index < 80u; ++index) { + schedule[index] = + sha2_64_small_sigma_1(schedule[index - 2u]) + schedule[index - 7u] + + sha2_64_small_sigma_0(schedule[index - 15u]) + schedule[index - 16u]; + } + + auto working = state; + + // Compression function (FIPS 180-4 Section 6.4.2 step 3) + for (std::uint64_t round_index = 0u; round_index < 80u; ++round_index) { + const auto temporary_1 = + working[7] + sha2_64_big_sigma_1(working[4]) + + sha2_64_choice(working[4], working[5], working[6]) + + round_constants[round_index] + schedule[round_index]; + const auto temporary_2 = + sha2_64_big_sigma_0(working[0]) + + sha2_64_majority(working[0], working[1], working[2]); + + working[7] = working[6]; + working[6] = working[5]; + working[5] = working[4]; + working[4] = working[3] + temporary_1; + working[3] = working[2]; + working[2] = working[1]; + working[1] = working[0]; + working[0] = temporary_1 + temporary_2; + } + + for (std::uint64_t index = 0u; index < 8u; ++index) { + state[index] += working[index]; + } +} + +inline auto sha2_64_hash(const std::string_view input, + std::array &state) -> void { + const auto *const input_bytes = + reinterpret_cast(input.data()); + const std::size_t input_length = input.size(); + + // Process all full 128-byte blocks directly from the input (streaming) + std::size_t processed_bytes = 0u; + while (input_length - processed_bytes >= 128u) { + sha2_64_process_block(input_bytes + processed_bytes, state); + processed_bytes += 128u; + } + + // Prepare the final block(s) (one or two 128-byte blocks) + std::array final_block{}; + const std::size_t remaining_bytes = input_length - processed_bytes; + if (remaining_bytes > 0u) { + std::memcpy(final_block.data(), input_bytes + processed_bytes, + remaining_bytes); + } + + // Append the 0x80 byte after the message data + final_block[remaining_bytes] = 0x80u; + + // Append length in bits as a big-endian 128-bit value at the end of the + // padding (FIPS 180-4 Section 5.1.2). The bit length of any input is at + // most 67 bits wide, so the upper word carries the bits that the 8x + // multiplication would otherwise overflow + const std::uint64_t message_length_bits_high = + static_cast(input_length) >> 61u; + const std::uint64_t message_length_bits_low = + static_cast(input_length) << 3u; + + if (remaining_bytes < 112u) { + for (std::uint64_t index = 0u; index < 8u; ++index) { + final_block[112u + index] = static_cast( + (message_length_bits_high >> (8u * (7u - index))) & 0xffu); + final_block[120u + index] = static_cast( + (message_length_bits_low >> (8u * (7u - index))) & 0xffu); + } + sha2_64_process_block(final_block.data(), state); + } else { + for (std::uint64_t index = 0u; index < 8u; ++index) { + final_block[128u + 112u + index] = static_cast( + (message_length_bits_high >> (8u * (7u - index))) & 0xffu); + final_block[128u + 120u + index] = static_cast( + (message_length_bits_low >> (8u * (7u - index))) & 0xffu); + } + + sha2_64_process_block(final_block.data(), state); + sha2_64_process_block(final_block.data() + 128u, state); + } +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_sha384.cc b/vendor/core/src/core/crypto/crypto_sha384.cc new file mode 100644 index 00000000..92d8dc43 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha384.cc @@ -0,0 +1,21 @@ +#include +#include + +#include // std::ostream, std::streamsize +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +auto sha384(const std::string_view input) -> std::string { + const auto digest{sha384_digest(input)}; + return bytes_to_hex( + {reinterpret_cast(digest.data()), digest.size()}); +} + +auto sha384(const std::string_view input, std::ostream &output) -> void { + const auto result = sha384(input); + output.write(result.data(), static_cast(result.size())); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha384_apple.cc b/vendor/core/src/core/crypto/crypto_sha384_apple.cc new file mode 100644 index 00000000..3ca2a0a3 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha384_apple.cc @@ -0,0 +1,36 @@ +#include + +#include // CC_SHA384*, CC_SHA512_CTX, CC_LONG + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits + +namespace sourcemeta::core { + +auto sha384_digest(const std::string_view input) + -> std::array { + CC_SHA512_CTX context; + CC_SHA384_Init(&context); + + // The platform update interface takes a 32-bit length, so larger + // inputs must be fed in chunks + const auto *remaining_data{input.data()}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + while (remaining_size > 0) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + CC_SHA384_Update(&context, remaining_data, + static_cast(chunk_size)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + CC_SHA384_Final(digest.data(), &context); + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha384_openssl.cc b/vendor/core/src/core/crypto/crypto_sha384_openssl.cc new file mode 100644 index 00000000..be700f91 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha384_openssl.cc @@ -0,0 +1,35 @@ +#include + +#include // EVP_* + +#include // std::array +#include // std::uint8_t +#include // std::runtime_error + +namespace sourcemeta::core { + +auto sha384_digest(const std::string_view input) + -> std::array { + auto *context = EVP_MD_CTX_new(); + if (context == nullptr) { + throw std::runtime_error("Could not allocate OpenSSL digest context"); + } + + if (EVP_DigestInit_ex(context, EVP_sha384(), nullptr) != 1 || + EVP_DigestUpdate(context, input.data(), input.size()) != 1) { + EVP_MD_CTX_free(context); + throw std::runtime_error("Could not compute SHA-384 digest"); + } + + std::array digest{}; + unsigned int length = 0; + if (EVP_DigestFinal_ex(context, digest.data(), &length) != 1) { + EVP_MD_CTX_free(context); + throw std::runtime_error("Could not finalize SHA-384 digest"); + } + + EVP_MD_CTX_free(context); + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha384_other.cc b/vendor/core/src/core/crypto/crypto_sha384_other.cc new file mode 100644 index 00000000..41f66df7 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha384_other.cc @@ -0,0 +1,35 @@ +#include + +#include "crypto_sha2_64.h" + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint64_t + +namespace sourcemeta::core { + +auto sha384_digest(const std::string_view input) + -> std::array { + // Initial hash values: first 64 bits of the fractional parts of the + // square roots of the 9th through 16th primes (FIPS 180-4 Section 5.3.4) + std::array state{ + {0xcbbb9d5dc1059ed8U, 0x629a292a367cd507U, 0x9159015a3070dd17U, + 0x152fecd8f70e5939U, 0x67332667ffc00b31U, 0x8eb44a8768581511U, + 0xdb0c2e0d64f98fa7U, 0x47b5481dbefa4fa4U}}; + + sha2_64_hash(input, state); + + // The digest is the leftmost 384 bits of the final state + // (FIPS 180-4 Section 6.5) + std::array digest{}; + for (std::size_t word_index = 0u; word_index < 6u; ++word_index) { + for (std::size_t byte_index = 0u; byte_index < 8u; ++byte_index) { + digest[(word_index * 8u) + byte_index] = static_cast( + (state[word_index] >> (8u * (7u - byte_index))) & 0xffu); + } + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha384_windows.cc b/vendor/core/src/core/crypto/crypto_sha384_windows.cc new file mode 100644 index 00000000..dc49e83c --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha384_windows.cc @@ -0,0 +1,64 @@ +#include + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG + +#include // BCrypt*, BCRYPT_* + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits +#include // std::runtime_error + +namespace sourcemeta::core { + +auto sha384_digest(const std::string_view input) + -> std::array { + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, BCRYPT_SHA384_ALGORITHM, nullptr, 0))) { + throw std::runtime_error("Could not open the CNG SHA-384 provider"); + } + + BCRYPT_HASH_HANDLE hash{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + throw std::runtime_error("Could not create the CNG SHA-384 hash"); + } + + // The data interface is not const-qualified but never writes through + // the pointer, and it takes a 32-bit length, so larger inputs must be + // fed in chunks + auto *remaining_data{ + reinterpret_cast(const_cast(input.data()))}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + auto success{true}; + while (remaining_size > 0 && success) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + success = BCRYPT_SUCCESS(BCryptHashData(hash, remaining_data, + static_cast(chunk_size), 0)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + if (success) { + success = BCRYPT_SUCCESS(BCryptFinishHash( + hash, digest.data(), static_cast(digest.size()), 0)); + } + + BCryptDestroyHash(hash); + BCryptCloseAlgorithmProvider(algorithm, 0); + if (!success) { + throw std::runtime_error("Could not compute the CNG SHA-384 digest"); + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha512.cc b/vendor/core/src/core/crypto/crypto_sha512.cc new file mode 100644 index 00000000..8dfc6258 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha512.cc @@ -0,0 +1,21 @@ +#include +#include + +#include // std::ostream, std::streamsize +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +auto sha512(const std::string_view input) -> std::string { + const auto digest{sha512_digest(input)}; + return bytes_to_hex( + {reinterpret_cast(digest.data()), digest.size()}); +} + +auto sha512(const std::string_view input, std::ostream &output) -> void { + const auto result = sha512(input); + output.write(result.data(), static_cast(result.size())); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha512_apple.cc b/vendor/core/src/core/crypto/crypto_sha512_apple.cc new file mode 100644 index 00000000..bd455604 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha512_apple.cc @@ -0,0 +1,36 @@ +#include + +#include // CC_SHA512*, CC_LONG + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits + +namespace sourcemeta::core { + +auto sha512_digest(const std::string_view input) + -> std::array { + CC_SHA512_CTX context; + CC_SHA512_Init(&context); + + // The platform update interface takes a 32-bit length, so larger + // inputs must be fed in chunks + const auto *remaining_data{input.data()}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + while (remaining_size > 0) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + CC_SHA512_Update(&context, remaining_data, + static_cast(chunk_size)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + CC_SHA512_Final(digest.data(), &context); + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha512_openssl.cc b/vendor/core/src/core/crypto/crypto_sha512_openssl.cc new file mode 100644 index 00000000..62ac80ed --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha512_openssl.cc @@ -0,0 +1,35 @@ +#include + +#include // EVP_* + +#include // std::array +#include // std::uint8_t +#include // std::runtime_error + +namespace sourcemeta::core { + +auto sha512_digest(const std::string_view input) + -> std::array { + auto *context = EVP_MD_CTX_new(); + if (context == nullptr) { + throw std::runtime_error("Could not allocate OpenSSL digest context"); + } + + if (EVP_DigestInit_ex(context, EVP_sha512(), nullptr) != 1 || + EVP_DigestUpdate(context, input.data(), input.size()) != 1) { + EVP_MD_CTX_free(context); + throw std::runtime_error("Could not compute SHA-512 digest"); + } + + std::array digest{}; + unsigned int length = 0; + if (EVP_DigestFinal_ex(context, digest.data(), &length) != 1) { + EVP_MD_CTX_free(context); + throw std::runtime_error("Could not finalize SHA-512 digest"); + } + + EVP_MD_CTX_free(context); + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha512_other.cc b/vendor/core/src/core/crypto/crypto_sha512_other.cc new file mode 100644 index 00000000..eb778a4f --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha512_other.cc @@ -0,0 +1,33 @@ +#include + +#include "crypto_sha2_64.h" + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint64_t + +namespace sourcemeta::core { + +auto sha512_digest(const std::string_view input) + -> std::array { + // Initial hash values: first 64 bits of the fractional parts of the + // square roots of the first 8 primes (FIPS 180-4 Section 5.3.5) + std::array state{ + {0x6a09e667f3bcc908U, 0xbb67ae8584caa73bU, 0x3c6ef372fe94f82bU, + 0xa54ff53a5f1d36f1U, 0x510e527fade682d1U, 0x9b05688c2b3e6c1fU, + 0x1f83d9abfb41bd6bU, 0x5be0cd19137e2179U}}; + + sha2_64_hash(input, state); + + std::array digest{}; + for (std::size_t word_index = 0u; word_index < 8u; ++word_index) { + for (std::size_t byte_index = 0u; byte_index < 8u; ++byte_index) { + digest[(word_index * 8u) + byte_index] = static_cast( + (state[word_index] >> (8u * (7u - byte_index))) & 0xffu); + } + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_sha512_windows.cc b/vendor/core/src/core/crypto/crypto_sha512_windows.cc new file mode 100644 index 00000000..23735698 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_sha512_windows.cc @@ -0,0 +1,64 @@ +#include + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG + +#include // BCrypt*, BCRYPT_* + +#include // std::array +#include // std::size_t +#include // std::uint8_t +#include // std::numeric_limits +#include // std::runtime_error + +namespace sourcemeta::core { + +auto sha512_digest(const std::string_view input) + -> std::array { + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, BCRYPT_SHA512_ALGORITHM, nullptr, 0))) { + throw std::runtime_error("Could not open the CNG SHA-512 provider"); + } + + BCRYPT_HASH_HANDLE hash{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + throw std::runtime_error("Could not create the CNG SHA-512 hash"); + } + + // The data interface is not const-qualified but never writes through + // the pointer, and it takes a 32-bit length, so larger inputs must be + // fed in chunks + auto *remaining_data{ + reinterpret_cast(const_cast(input.data()))}; + auto remaining_size{input.size()}; + constexpr std::size_t maximum_chunk{std::numeric_limits::max()}; + auto success{true}; + while (remaining_size > 0 && success) { + const auto chunk_size{remaining_size > maximum_chunk ? maximum_chunk + : remaining_size}; + success = BCRYPT_SUCCESS(BCryptHashData(hash, remaining_data, + static_cast(chunk_size), 0)); + remaining_data += chunk_size; + remaining_size -= chunk_size; + } + + std::array digest{}; + if (success) { + success = BCRYPT_SUCCESS(BCryptFinishHash( + hash, digest.data(), static_cast(digest.size()), 0)); + } + + BCryptDestroyHash(hash); + BCryptCloseAlgorithmProvider(algorithm, 0); + if (!success) { + throw std::runtime_error("Could not compute the CNG SHA-512 digest"); + } + + return digest; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_uuid.cc b/vendor/core/src/core/crypto/crypto_uuid.cc index f572438d..c0aac888 100644 --- a/vendor/core/src/core/crypto/crypto_uuid.cc +++ b/vendor/core/src/core/crypto/crypto_uuid.cc @@ -1,26 +1,13 @@ #include +#include // is_hex_digit + +#include "crypto_random.h" #include // std::array #include // std::size_t +#include // std::string #include // std::string_view -namespace { - -constexpr auto is_hex_digit(const char character) -> bool { - return (character >= '0' && character <= '9') || - (character >= 'a' && character <= 'f') || - (character >= 'A' && character <= 'F'); -} - -} // namespace - -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL -#include // RAND_bytes -#include // std::runtime_error -#else -#include // std::random_device, std::mt19937, std::uniform_int_distribution -#endif - namespace sourcemeta::core { // See RFC 9562 Section 5.4 @@ -33,20 +20,8 @@ auto uuidv4() -> std::string { {false, false, false, false, true, false, true, false, true, false, true, false, false, false, false, false}}; -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL std::array random_bytes{}; - if (RAND_bytes(random_bytes.data(), static_cast(random_bytes.size())) != - 1) { - throw std::runtime_error("Could not generate random bytes with OpenSSL"); - } -#else - thread_local std::random_device device; - thread_local std::mt19937 generator{device()}; - std::uniform_int_distribution distribution(0, - 15); - std::uniform_int_distribution - variant_distribution(0, 3); -#endif + fill_random_bytes(random_bytes); std::string result; result.reserve(36); @@ -55,34 +30,20 @@ auto uuidv4() -> std::string { result += '-'; } -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL const auto high_nibble = (random_bytes[index] >> 4u) & 0x0fu; const auto low_nibble = random_bytes[index] & 0x0fu; -#endif // RFC 9562 Section 5.4: version bits (48-51) must be 0b0100 if (index == 6) { result += '4'; // RFC 9562 Section 5.4: variant bits (64-65) must be 0b10 } else if (index == 8) { -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL result += variant_digits[high_nibble & 0x03u]; -#else - result += variant_digits[variant_distribution(generator)]; -#endif } else { -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL result += digits[high_nibble]; -#else - result += digits[distribution(generator)]; -#endif } -#ifdef SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL result += digits[low_nibble]; -#else - result += digits[distribution(generator)]; -#endif } return result; diff --git a/vendor/core/src/core/crypto/crypto_verify_ecdsa_apple.cc b/vendor/core/src/core/crypto/crypto_verify_ecdsa_apple.cc new file mode 100644 index 00000000..5a23aba1 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_ecdsa_apple.cc @@ -0,0 +1,135 @@ +#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 new file mode 100644 index 00000000..f9741721 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_ecdsa_openssl.cc @@ -0,0 +1,161 @@ +#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 new file mode 100644 index 00000000..df2a428a --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_ecdsa_other.cc @@ -0,0 +1,119 @@ +#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 new file mode 100644 index 00000000..6654099d --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_ecdsa_windows.cc @@ -0,0 +1,107 @@ +#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_rsa_apple.cc b/vendor/core/src/core/crypto/crypto_verify_rsa_apple.cc new file mode 100644 index 00000000..6b58acff --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_rsa_apple.cc @@ -0,0 +1,150 @@ +#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 new file mode 100644 index 00000000..52e55511 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_rsa_openssl.cc @@ -0,0 +1,144 @@ +#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 new file mode 100644 index 00000000..e70682ec --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_rsa_other.cc @@ -0,0 +1,258 @@ +#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 new file mode 100644 index 00000000..8b637964 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_rsa_windows.cc @@ -0,0 +1,137 @@ +#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/include/sourcemeta/core/crypto.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto.h index 550e949e..bab7ef2e 100644 --- a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto.h +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto.h @@ -2,7 +2,7 @@ #define SOURCEMETA_CORE_CRYPTO_H_ /// @defgroup crypto Crypto -/// @brief Cryptographic hash functions and UUID generation. +/// @brief Cryptographic hash functions, UUID generation, and Base64 codecs. /// /// This functionality is included as follows: /// @@ -10,10 +10,14 @@ /// #include /// ``` +#include #include #include #include #include +#include +#include #include +#include #endif diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h new file mode 100644 index 00000000..d54c90c6 --- /dev/null +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_base64.h @@ -0,0 +1,104 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_BASE64_H_ +#define SOURCEMETA_CORE_CRYPTO_BASE64_H_ + +#ifndef SOURCEMETA_CORE_CRYPTO_EXPORT +#include +#endif + +#include // std::optional +#include // std::ostream +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup crypto +/// Encode a byte sequence using Base64 (RFC 4648 Section 4) into a stream. +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::ostringstream result; +/// sourcemeta::core::base64_encode("foobar", result); +/// assert(result.str() == "Zm9vYmFy"); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT base64_encode(const std::string_view input, + std::ostream &output) -> void; + +/// @ingroup crypto +/// Encode a byte sequence using Base64 (RFC 4648 Section 4). For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::base64_encode("foobar") == "Zm9vYmFy"); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT base64_encode(const std::string_view input) + -> std::string; + +/// @ingroup crypto +/// Decode a Base64 string (RFC 4648 Section 4), returning no value unless the +/// input is a canonical padded encoding. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto result{sourcemeta::core::base64_decode("Zm9vYmFy")}; +/// assert(result.has_value()); +/// assert(result.value() == "foobar"); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT base64_decode(const std::string_view input) + -> std::optional; + +/// @ingroup crypto +/// Encode a byte sequence using unpadded Base64url (RFC 4648 Section 5) into a +/// stream. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::ostringstream result; +/// sourcemeta::core::base64url_encode("fo", result); +/// assert(result.str() == "Zm8"); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +base64url_encode(const std::string_view input, std::ostream &output) -> void; + +/// @ingroup crypto +/// Encode a byte sequence using unpadded Base64url (RFC 4648 Section 5). For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::base64url_encode("fo") == "Zm8"); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +base64url_encode(const std::string_view input) -> std::string; + +/// @ingroup crypto +/// Decode an unpadded Base64url string (RFC 4648 Section 5), returning no +/// value unless the input is a canonical encoding. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto result{sourcemeta::core::base64url_decode("Zm8")}; +/// assert(result.has_value()); +/// assert(result.value() == "fo"); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +base64url_decode(const std::string_view input) -> std::optional; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha256.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha256.h index 7fbe9976..4a73f162 100644 --- a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha256.h +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha256.h @@ -5,6 +5,8 @@ #include #endif +#include // std::array +#include // std::uint8_t #include // std::ostream #include // std::string #include // std::string_view @@ -39,6 +41,19 @@ auto SOURCEMETA_CORE_CRYPTO_EXPORT sha256(const std::string_view input, auto SOURCEMETA_CORE_CRYPTO_EXPORT sha256(const std::string_view input) -> std::string; +/// @ingroup crypto +/// Hash a string using SHA-256, returning the raw digest bytes. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto digest{sourcemeta::core::sha256_digest("foo bar")}; +/// assert(digest.size() == 32); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT sha256_digest(const std::string_view input) + -> std::array; + } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha384.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha384.h new file mode 100644 index 00000000..ae43981f --- /dev/null +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha384.h @@ -0,0 +1,59 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_SHA384_H_ +#define SOURCEMETA_CORE_CRYPTO_SHA384_H_ + +#ifndef SOURCEMETA_CORE_CRYPTO_EXPORT +#include +#endif + +#include // std::array +#include // std::uint8_t +#include // std::ostream +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup crypto +/// Hash a string using SHA-384. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::ostringstream result; +/// sourcemeta::core::sha384("foo bar", result); +/// std::cout << result.str() << "\n"; +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT sha384(const std::string_view input, + std::ostream &output) -> void; + +/// @ingroup crypto +/// Hash a string using SHA-384, returning the hex digest as a string. +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// std::cout << sourcemeta::core::sha384("foo bar") << "\n"; +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT sha384(const std::string_view input) + -> std::string; + +/// @ingroup crypto +/// Hash a string using SHA-384, returning the raw digest bytes. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto digest{sourcemeta::core::sha384_digest("foo bar")}; +/// assert(digest.size() == 48); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT sha384_digest(const std::string_view input) + -> std::array; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha512.h b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha512.h new file mode 100644 index 00000000..c83b5f52 --- /dev/null +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_sha512.h @@ -0,0 +1,59 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_SHA512_H_ +#define SOURCEMETA_CORE_CRYPTO_SHA512_H_ + +#ifndef SOURCEMETA_CORE_CRYPTO_EXPORT +#include +#endif + +#include // std::array +#include // std::uint8_t +#include // std::ostream +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup crypto +/// Hash a string using SHA-512. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::ostringstream result; +/// sourcemeta::core::sha512("foo bar", result); +/// std::cout << result.str() << "\n"; +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT sha512(const std::string_view input, + std::ostream &output) -> void; + +/// @ingroup crypto +/// Hash a string using SHA-512, returning the hex digest as a string. +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// std::cout << sourcemeta::core::sha512("foo bar") << "\n"; +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT sha512(const std::string_view input) + -> std::string; + +/// @ingroup crypto +/// Hash a string using SHA-512, returning the raw digest bytes. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto digest{sourcemeta::core::sha512_digest("foo bar")}; +/// assert(digest.size() == 64); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT sha512_digest(const std::string_view input) + -> std::array; + +} // namespace sourcemeta::core + +#endif 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 new file mode 100644 index 00000000..6a033bef --- /dev/null +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_verify.h @@ -0,0 +1,84 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_VERIFY_H_ +#define SOURCEMETA_CORE_CRYPTO_VERIFY_H_ + +#ifndef SOURCEMETA_CORE_CRYPTO_EXPORT +#include +#endif + +#include // std::uint8_t +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup crypto +/// The hash functions supported by signature verification. +enum class SignatureHashFunction : std::uint8_t { SHA256, SHA384, SHA512 }; + +/// @ingroup crypto +/// The NIST elliptic curves supported by signature verification. +enum class EllipticCurve : std::uint8_t { P256, P384, P521 }; + +/// @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: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(!sourcemeta::core::rsassa_pkcs1_v15_verify( +/// 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; + +/// @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: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(!sourcemeta::core::rsassa_pss_verify( +/// 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; + +/// @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: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(!sourcemeta::core::ecdsa_verify( +/// sourcemeta::core::EllipticCurve::P256, +/// 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 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 8203b1ce..ddc5d7d1 100644 --- a/vendor/core/src/core/http/CMakeLists.txt +++ b/vendor/core/src/core/http/CMakeLists.txt @@ -2,7 +2,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME http PRIVATE_HEADERS problem.h status.h method.h message.h error.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) + accept_includes_all.cc content_type_matches.cc parse_bearer.cc) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME http) 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 53c22d1c..59028c61 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http.h @@ -250,6 +250,24 @@ auto http_field_list_contains_any( const std::string_view header_value, std::initializer_list tokens) noexcept -> bool; +/// @ingroup http +/// Extract the credential from an `Authorization` header that uses the Bearer +/// scheme per RFC 6750 §2.1, matching the scheme case-insensitively per RFC +/// 9110 §11.1 and tolerating optional whitespace around the token. Returns an +/// empty view when the header is absent, uses another scheme, or does not carry +/// a well-formed `b64token` credential. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_parse_bearer("Bearer abc123") == "abc123"); +/// assert(sourcemeta::core::http_parse_bearer("Basic abc123").empty()); +/// ``` +SOURCEMETA_CORE_HTTP_EXPORT +auto http_parse_bearer(const std::string_view authorization) noexcept + -> std::string_view; + } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/http/parse_bearer.cc b/vendor/core/src/core/http/parse_bearer.cc new file mode 100644 index 00000000..aaa3ca48 --- /dev/null +++ b/vendor/core/src/core/http/parse_bearer.cc @@ -0,0 +1,68 @@ +#include + +#include "helpers.h" + +#include // std::size_t +#include // std::string_view + +namespace { + +auto is_b64token_character(const char character) noexcept -> bool { + return (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z') || + (character >= '0' && character <= '9') || character == '-' || + character == '.' || character == '_' || character == '~' || + character == '+' || character == '/'; +} + +// RFC 6750 §2.1: b64token = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / +// "/" ) *"=". At least one character from the alphabet, followed by optional +// trailing padding. +auto is_b64token(const std::string_view token) noexcept -> bool { + std::size_t position{0}; + while (position < token.size() && is_b64token_character(token[position])) { + ++position; + } + + if (position == 0) { + return false; + } + + while (position < token.size()) { + if (token[position] != '=') { + return false; + } + ++position; + } + + return true; +} + +} // namespace + +namespace sourcemeta::core { + +auto http_parse_bearer(const std::string_view authorization) noexcept + -> std::string_view { + constexpr std::string_view scheme{"bearer"}; + if (authorization.size() <= scheme.size() || + authorization[scheme.size()] != ' ') { + return {}; + } + + if (!http_iequals_ascii(http_subview(authorization, 0, scheme.size()), + scheme)) { + return {}; + } + + const auto token{http_trim_trailing_ows(http_trim_leading_ows( + http_subview(authorization, scheme.size() + 1, + authorization.size() - scheme.size() - 1)))}; + if (!is_b64token(token)) { + return {}; + } + + return token; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/CMakeLists.txt b/vendor/core/src/core/jose/CMakeLists.txt new file mode 100644 index 00000000..61e8195c --- /dev/null +++ b/vendor/core/src/core/jose/CMakeLists.txt @@ -0,0 +1,12 @@ +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) + +target_link_libraries(sourcemeta_core_jose + PUBLIC sourcemeta::core::json) +target_link_libraries(sourcemeta_core_jose + PRIVATE sourcemeta::core::crypto) + +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME jose) +endif() diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose.h new file mode 100644 index 00000000..c67ef0d8 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose.h @@ -0,0 +1,24 @@ +#ifndef SOURCEMETA_CORE_JOSE_H_ +#define SOURCEMETA_CORE_JOSE_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +#include +#include +// NOLINTEND(misc-include-cleaner) + +/// @defgroup jose JOSE +/// @brief Standards-driven primitives for validating JSON Web Tokens. +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +#endif 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 new file mode 100644 index 00000000..0172d01b --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h @@ -0,0 +1,47 @@ +#ifndef SOURCEMETA_CORE_JOSE_ALGORITHM_H_ +#define SOURCEMETA_CORE_JOSE_ALGORITHM_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +#include // std::uint8_t +#include // std::optional +#include // std::string_view + +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. +enum class JWSAlgorithm : std::uint8_t { + RS256, + RS384, + RS512, + PS256, + PS384, + PS512, + ES256, + ES384, + ES512 +}; + +/// @ingroup jose +/// Map a JSON Web Signature `alg` value to its algorithm, returning no value +/// for any unrecognized name. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::to_jws_algorithm("RS256").has_value()); +/// assert(!sourcemeta::core::to_jws_algorithm("none").has_value()); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto to_jws_algorithm(const std::string_view value) noexcept + -> std::optional; + +} // namespace sourcemeta::core + +#endif 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 new file mode 100644 index 00000000..aa56fdef --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_error.h @@ -0,0 +1,40 @@ +#ifndef SOURCEMETA_CORE_JOSE_ERROR_H_ +#define SOURCEMETA_CORE_JOSE_ERROR_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +#include // std::exception + +namespace sourcemeta::core { + +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup jose +/// An error that occurs when parsing an invalid JSON Web Key. +class SOURCEMETA_CORE_JOSE_EXPORT JWKParseError : public std::exception { +public: + [[nodiscard]] auto what() const noexcept -> const char * override { + return "The input is not a valid JSON Web Key"; + } +}; + +/// @ingroup jose +/// An error that occurs when parsing an invalid JSON Web Key Set. +class SOURCEMETA_CORE_JOSE_EXPORT JWKSParseError : public std::exception { +public: + [[nodiscard]] auto what() const noexcept -> const char * override { + return "The input is not a valid JSON Web Key Set"; + } +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::core + +#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 new file mode 100644 index 00000000..3a315c35 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h @@ -0,0 +1,115 @@ +#ifndef SOURCEMETA_CORE_JOSE_JWK_H_ +#define SOURCEMETA_CORE_JOSE_JWK_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include + +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view + +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: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto document{sourcemeta::core::parse_json( +/// R"({ "kty": "RSA", "n": "0vx7ag", "e": "AQAB" })")}; +/// const auto key{sourcemeta::core::JWK::from(document)}; +/// assert(key.has_value()); +/// assert(key.value().type() == sourcemeta::core::JWK::Type::RSA); +/// ``` +class SOURCEMETA_CORE_JOSE_EXPORT JWK { +public: + enum class Type : std::uint8_t { RSA, EllipticCurve }; + + /// Parse a JSON Web Key from a JSON value, throwing on invalid input. + explicit JWK(const JSON &value); + + /// Parse a JSON Web Key from a JSON value, throwing on invalid input. + explicit JWK(JSON &&value); + + /// Parse a JSON Web Key from a JSON value, returning no value on invalid + /// input. + [[nodiscard]] static auto from(const JSON &value) -> std::optional; + + /// Parse a JSON Web Key from a JSON value, returning no value on invalid + /// input. + [[nodiscard]] static auto from(JSON &&value) -> std::optional; + + [[nodiscard]] auto type() const noexcept -> Type { return this->type_; } + + [[nodiscard]] auto key_id() const noexcept + -> std::optional { + if (this->key_id_.has_value()) { + return std::string_view{this->key_id_.value()}; + } + + return std::nullopt; + } + + [[nodiscard]] auto algorithm() const noexcept -> std::optional { + 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 + [[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_; + } + +private: + JWK() = default; + static auto parse(const JSON &value, JWK &result) -> bool; + +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + 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_; +#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_jwks.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h new file mode 100644 index 00000000..8dc94084 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h @@ -0,0 +1,83 @@ +#ifndef SOURCEMETA_CORE_JOSE_JWKS_H_ +#define SOURCEMETA_CORE_JOSE_JWKS_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +// NOLINTEND(misc-include-cleaner) + +#include +#include + +#include // std::size_t +#include // std::optional +#include // std::string_view +#include // std::vector + +namespace sourcemeta::core { + +/// @ingroup jose +/// A parsed JSON Web Key Set (RFC 7517 Section 5). Keys that individually fail +/// to parse, such as those of an unsupported type, are skipped rather than +/// failing the whole set, so one exotic key cannot break verification of +/// tokens signed by the others. The set owns its keys. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto document{sourcemeta::core::parse_json( +/// R"({ "keys": [ { "kty": "RSA", "n": "0vx7ag", "e": "AQAB", +/// "kid": "2024" } ] })")}; +/// const auto keys{sourcemeta::core::JWKS::from(document)}; +/// assert(keys.has_value()); +/// assert(keys.value().find("2024") != nullptr); +/// ``` +class SOURCEMETA_CORE_JOSE_EXPORT JWKS { +public: + /// Parse a JSON Web Key Set from a JSON value, throwing a `JWKSParseError` + /// on invalid input. + explicit JWKS(const JSON &value); + explicit JWKS(JSON &&value); + + /// 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; + [[nodiscard]] static auto from(JSON &&value) -> std::optional; + + /// Look up a key by its identifier (RFC 7515 Section 4.1.4), returning no + /// pointer when no key in the set carries it. + [[nodiscard]] auto find(const std::string_view key_id) const noexcept + -> const JWK *; + + [[nodiscard]] auto size() const noexcept -> std::size_t { + return this->keys_.size(); + } + + [[nodiscard]] auto empty() const noexcept -> bool { + return this->keys_.empty(); + } + + [[nodiscard]] auto begin() const noexcept { return this->keys_.begin(); } + [[nodiscard]] auto end() const noexcept { return this->keys_.end(); } + +private: + JWKS() = default; + static auto parse(const JSON &value, JWKS &result) -> bool; + +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + std::vector keys_; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif +}; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jose/jose_algorithm.cc b/vendor/core/src/core/jose/jose_algorithm.cc new file mode 100644 index 00000000..e7e39c89 --- /dev/null +++ b/vendor/core/src/core/jose/jose_algorithm.cc @@ -0,0 +1,33 @@ +#include + +#include // std::optional, std::nullopt +#include // std::string_view + +namespace sourcemeta::core { + +auto to_jws_algorithm(const std::string_view value) noexcept + -> std::optional { + if (value == "RS256") { + return JWSAlgorithm::RS256; + } else if (value == "RS384") { + return JWSAlgorithm::RS384; + } else if (value == "RS512") { + return JWSAlgorithm::RS512; + } else if (value == "PS256") { + return JWSAlgorithm::PS256; + } else if (value == "PS384") { + return JWSAlgorithm::PS384; + } else if (value == "PS512") { + return JWSAlgorithm::PS512; + } else if (value == "ES256") { + return JWSAlgorithm::ES256; + } else if (value == "ES384") { + return JWSAlgorithm::ES384; + } else if (value == "ES512") { + return JWSAlgorithm::ES512; + } else { + return std::nullopt; + } +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwk.cc b/vendor/core/src/core/jose/jose_jwk.cc new file mode 100644 index 00000000..4d581ee0 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwk.cc @@ -0,0 +1,202 @@ +#include + +#include + +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string_view +#include // std::move, std::unreachable + +namespace { + +const auto HASH_KTY{sourcemeta::core::JSON::Object::hash("kty")}; +const auto HASH_N{sourcemeta::core::JSON::Object::hash("n")}; +const auto HASH_E{sourcemeta::core::JSON::Object::hash("e")}; +const auto HASH_CRV{sourcemeta::core::JSON::Object::hash("crv")}; +const auto HASH_X{sourcemeta::core::JSON::Object::hash("x")}; +const auto HASH_Y{sourcemeta::core::JSON::Object::hash("y")}; +const auto HASH_KID{sourcemeta::core::JSON::Object::hash("kid")}; +const auto HASH_ALG{sourcemeta::core::JSON::Object::hash("alg")}; +const auto HASH_D{sourcemeta::core::JSON::Object::hash("d")}; +const auto HASH_P{sourcemeta::core::JSON::Object::hash("p")}; +const auto HASH_Q{sourcemeta::core::JSON::Object::hash("q")}; +const auto HASH_DP{sourcemeta::core::JSON::Object::hash("dp")}; +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) +auto algorithm_matches_key(const sourcemeta::core::JWSAlgorithm algorithm, + const sourcemeta::core::JWK::Type type, + const std::string_view curve) -> bool { + switch (algorithm) { + case sourcemeta::core::JWSAlgorithm::RS256: + case sourcemeta::core::JWSAlgorithm::RS384: + case sourcemeta::core::JWSAlgorithm::RS512: + case sourcemeta::core::JWSAlgorithm::PS256: + case sourcemeta::core::JWSAlgorithm::PS384: + case sourcemeta::core::JWSAlgorithm::PS512: + return type == sourcemeta::core::JWK::Type::RSA; + case sourcemeta::core::JWSAlgorithm::ES256: + return type == sourcemeta::core::JWK::Type::EllipticCurve && + curve == "P-256"; + case sourcemeta::core::JWSAlgorithm::ES384: + return type == sourcemeta::core::JWK::Type::EllipticCurve && + curve == "P-384"; + case sourcemeta::core::JWSAlgorithm::ES512: + return type == sourcemeta::core::JWK::Type::EllipticCurve && + curve == "P-521"; + } + + std::unreachable(); +} + +// The coordinate octet length is fixed per curve (RFC 7518 Section 6.2.1.2) +auto ec_coordinate_bytes(const std::string_view curve) + -> std::optional { + if (curve == "P-256") { + return 32; + } else if (curve == "P-384") { + return 48; + } else if (curve == "P-521") { + return 66; + } else { + return std::nullopt; + } +} + +} // namespace + +namespace sourcemeta::core { + +auto JWK::parse(const JSON &value, JWK &result) -> bool { + if (!value.is_object()) { + return false; + } + + const auto *key_type{value.try_at("kty", HASH_KTY)}; + if (key_type == nullptr || !key_type->is_string()) { + return false; + } + + const auto &key_type_value{key_type->to_string()}; + 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 + if (value.try_at("d", HASH_D) != nullptr || + value.try_at("p", HASH_P) != nullptr || + value.try_at("q", HASH_Q) != nullptr || + value.try_at("dp", HASH_DP) != nullptr || + value.try_at("dq", HASH_DQ) != nullptr || + value.try_at("qi", HASH_QI) != nullptr || + value.try_at("oth", HASH_OTH) != nullptr) { + return false; + } + + const auto *modulus{value.try_at("n", HASH_N)}; + const auto *exponent{value.try_at("e", HASH_E)}; + if (modulus == nullptr || !modulus->is_string() || exponent == nullptr || + !exponent->is_string()) { + return false; + } + + auto decoded_modulus{base64url_decode(modulus->to_string())}; + auto decoded_exponent{base64url_decode(exponent->to_string())}; + if (!decoded_modulus.has_value() || decoded_modulus.value().empty() || + !decoded_exponent.has_value() || decoded_exponent.value().empty()) { + return false; + } + + result.type_ = Type::RSA; + result.modulus_ = std::move(decoded_modulus).value(); + result.exponent_ = std::move(decoded_exponent).value(); + } else if (key_type_value == "EC") { + // A public key must not carry the private parameter (RFC 7518 Section + // 6.2.2) + if (value.try_at("d", HASH_D) != nullptr) { + return false; + } + + const auto *curve{value.try_at("crv", HASH_CRV)}; + const auto *coordinate_x{value.try_at("x", HASH_X)}; + const auto *coordinate_y{value.try_at("y", HASH_Y)}; + if (curve == nullptr || !curve->is_string() || coordinate_x == nullptr || + !coordinate_x->is_string() || coordinate_y == nullptr || + !coordinate_y->is_string()) { + return false; + } + + const auto coordinate_bytes{ec_coordinate_bytes(curve->to_string())}; + if (!coordinate_bytes.has_value()) { + return false; + } + + auto decoded_x{base64url_decode(coordinate_x->to_string())}; + auto decoded_y{base64url_decode(coordinate_y->to_string())}; + if (!decoded_x.has_value() || + decoded_x.value().size() != coordinate_bytes.value() || + !decoded_y.has_value() || + decoded_y.value().size() != coordinate_bytes.value()) { + return false; + } + + 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(); + } else { + return false; + } + + const auto *key_id{value.try_at("kid", HASH_KID)}; + if (key_id != nullptr) { + if (!key_id->is_string()) { + return false; + } + + result.key_id_ = key_id->to_string(); + } + + const auto *algorithm{value.try_at("alg", HASH_ALG)}; + if (algorithm != nullptr) { + if (!algorithm->is_string()) { + return false; + } + + // The algorithm is an advisory hint (RFC 7517 Section 4.4), so honor it + // only when it names a supported algorithm consistent with the key type, + // and otherwise leave it unset rather than rejecting an otherwise valid key + const auto parsed{to_jws_algorithm(algorithm->to_string())}; + if (parsed.has_value() && + algorithm_matches_key(parsed.value(), result.type_, result.curve_)) { + result.algorithm_ = parsed; + } + } + + return true; +} + +JWK::JWK(const JSON &value) { + if (!parse(value, *this)) { + throw JWKParseError{}; + } +} + +// The key material is base64url-decoded into fresh storage, so there is nothing +// to move out of the source value. The rvalue overloads exist for call-site +// symmetry and delegate to the lvalue path +JWK::JWK(JSON &&value) : JWK{value} {} + +auto JWK::from(const JSON &value) -> std::optional { + JWK result; + if (parse(value, result)) { + return result; + } + + return std::nullopt; +} + +auto JWK::from(JSON &&value) -> std::optional { return from(value); } + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwks.cc b/vendor/core/src/core/jose/jose_jwks.cc new file mode 100644 index 00000000..17afbe79 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwks.cc @@ -0,0 +1,72 @@ +#include + +#include // std::optional, std::nullopt +#include // std::string_view +#include // std::move + +namespace { + +const auto HASH_KEYS{sourcemeta::core::JSON::Object::hash("keys")}; + +} // namespace + +namespace sourcemeta::core { + +auto JWKS::parse(const JSON &value, JWKS &result) -> bool { + if (!value.is_object()) { + return false; + } + + const auto *keys{value.try_at("keys", HASH_KEYS)}; + if (keys == nullptr || !keys->is_array()) { + return false; + } + + // Individual keys that fail to parse are skipped so that one exotic key + // cannot break verification of tokens signed by the others (RFC 7517 + // Section 5) + for (const auto &entry : keys->as_array()) { + auto key{JWK::from(entry)}; + if (key.has_value()) { + result.keys_.push_back(std::move(key).value()); + } + } + + // An empty set, or one whose keys all failed to parse, is not usable + return !result.keys_.empty(); +} + +JWKS::JWKS(const JSON &value) { + if (!parse(value, *this)) { + throw JWKSParseError{}; + } +} + +// The keys are decoded into fresh storage, so there is nothing to move out of +// the source value. The rvalue overloads exist for call-site symmetry and +// delegate to the lvalue path +JWKS::JWKS(JSON &&value) : JWKS{value} {} + +auto JWKS::from(const JSON &value) -> std::optional { + JWKS result; + if (parse(value, result)) { + return result; + } + + return std::nullopt; +} + +auto JWKS::from(JSON &&value) -> std::optional { return from(value); } + +auto JWKS::find(const std::string_view key_id) const noexcept -> const JWK * { + for (const auto &key : this->keys_) { + const auto candidate{key.key_id()}; + if (candidate.has_value() && candidate.value() == key_id) { + return &key; + } + } + + return nullptr; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/time/CMakeLists.txt b/vendor/core/src/core/time/CMakeLists.txt index bfd35d61..0de159ad 100644 --- a/vendor/core/src/core/time/CMakeLists.txt +++ b/vendor/core/src/core/time/CMakeLists.txt @@ -1,7 +1,8 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME time SOURCES helpers.h imf_fixdate.cc rfc850_date.cc asctime.cc - rfc3339_datetime.cc rfc3339_fulldate.cc rfc3339_fulltime.cc - rfc3339_partialtime_no_secfrac.cc rfc3339_duration.cc) + unix_timestamp.cc rfc3339_datetime.cc rfc3339_fulldate.cc + rfc3339_fulltime.cc rfc3339_partialtime_no_secfrac.cc + rfc3339_duration.cc) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME time) diff --git a/vendor/core/src/core/time/include/sourcemeta/core/time.h b/vendor/core/src/core/time/include/sourcemeta/core/time.h index 26acc75a..7751f70f 100644 --- a/vendor/core/src/core/time/include/sourcemeta/core/time.h +++ b/vendor/core/src/core/time/include/sourcemeta/core/time.h @@ -118,6 +118,45 @@ SOURCEMETA_CORE_TIME_EXPORT auto from_asctime(const std::string_view value) noexcept -> std::optional; +/// @ingroup time +/// Convert a POSIX timestamp, the number of seconds since the Unix epoch +/// ignoring leap seconds and possibly fractional, into a time point, returning +/// no value when the timestamp is not representable. Fractional seconds finer +/// than the time point's tick resolution are truncated towards zero. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto point{ +/// sourcemeta::core::from_unix_timestamp(std::chrono::duration{0})}; +/// assert(point.has_value()); +/// assert(point.value() == std::chrono::system_clock::from_time_t(0)); +/// ``` +SOURCEMETA_CORE_TIME_EXPORT +auto from_unix_timestamp(const std::chrono::duration seconds) noexcept + -> std::optional; + +/// @ingroup time +/// Convert a time point into a POSIX timestamp, the number of seconds since +/// the Unix epoch ignoring leap seconds. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto point{std::chrono::system_clock::from_time_t(0)}; +/// assert(sourcemeta::core::to_unix_timestamp(point) == +/// std::chrono::duration{0}); +/// ``` +SOURCEMETA_CORE_TIME_EXPORT +auto to_unix_timestamp( + const std::chrono::system_clock::time_point time) noexcept + -> std::chrono::duration; + /// @ingroup time /// Check whether the given string is a valid date-time value per RFC 3339 /// Section 5.6 (Internet Date/Time Format). This implements the full diff --git a/vendor/core/src/core/time/unix_timestamp.cc b/vendor/core/src/core/time/unix_timestamp.cc new file mode 100644 index 00000000..e7daeb84 --- /dev/null +++ b/vendor/core/src/core/time/unix_timestamp.cc @@ -0,0 +1,41 @@ +#include + +#include // std::chrono::duration, std::chrono::system_clock +#include // std::isfinite +#include // std::optional, std::nullopt + +namespace sourcemeta::core { + +auto from_unix_timestamp(const std::chrono::duration seconds) noexcept + -> std::optional { + if (!std::isfinite(seconds.count())) { + return std::nullopt; + } + + // Reject timestamps outside the clock's representable window, leaving a one + // second guard so that the conversion to the clock's native tick cannot + // overflow at the boundary + constexpr auto maximum{ + std::chrono::duration_cast>( + std::chrono::system_clock::duration::max()) - + std::chrono::duration{1}}; + constexpr auto minimum{ + std::chrono::duration_cast>( + std::chrono::system_clock::duration::min()) + + std::chrono::duration{1}}; + if (seconds < minimum || seconds > maximum) { + return std::nullopt; + } + + return std::chrono::system_clock::time_point{ + std::chrono::duration_cast(seconds)}; +} + +auto to_unix_timestamp( + const std::chrono::system_clock::time_point time) noexcept + -> std::chrono::duration { + return std::chrono::duration_cast>( + time.time_since_epoch()); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/lang/io/include/sourcemeta/core/io.h b/vendor/core/src/lang/io/include/sourcemeta/core/io.h index 6823a2e3..771b4d0b 100644 --- a/vendor/core/src/lang/io/include/sourcemeta/core/io.h +++ b/vendor/core/src/lang/io/include/sourcemeta/core/io.h @@ -233,8 +233,10 @@ inline auto read_stdin() -> std::string { return read_to_string(std::cin); } /// @ingroup io /// -/// Iterate the lines of `stream`, invoking `callback` with each line. The -/// line view is only valid for the duration of the callback. For example: +/// Iterate the lines of `stream`, invoking `callback` with each line. A +/// trailing carriage return is dropped so that Windows line endings produce +/// the same line as Unix ones. The line view is only valid for the duration +/// of the callback. For example: /// /// ```cpp /// #include @@ -251,7 +253,12 @@ template auto for_each_line(std::istream &stream, Callback callback) -> void { std::string line; while (std::getline(stream, line)) { - callback(std::string_view{line}); + std::string_view view{line}; + if (!view.empty() && view.back() == '\r') { + view.remove_suffix(1); + } + + callback(view); } } 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 9c5766bc..1f6e3749 100644 --- a/vendor/core/src/lang/text/include/sourcemeta/core/text.h +++ b/vendor/core/src/lang/text/include/sourcemeta/core/text.h @@ -204,6 +204,54 @@ auto truncate(std::string &input, const std::size_t maximum_length, SOURCEMETA_CORE_TEXT_EXPORT auto trim(const std::string_view input) noexcept -> std::string_view; +/// @ingroup text +/// +/// Return `input` with leading occurrences of `character` removed. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::strip_left("000123", '0') == "123"); +/// assert(sourcemeta::core::strip_left("abc", '0') == "abc"); +/// ``` +SOURCEMETA_CORE_TEXT_EXPORT +auto strip_left(const std::string_view input, const char character) noexcept + -> std::string_view; + +/// @ingroup text +/// +/// Return `input` with trailing occurrences of `character` removed. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::strip_right("hello\r\r", '\r') == "hello"); +/// assert(sourcemeta::core::strip_right("abc", '\r') == "abc"); +/// ``` +SOURCEMETA_CORE_TEXT_EXPORT +auto strip_right(const std::string_view input, const char character) noexcept + -> std::string_view; + +/// @ingroup text +/// +/// Return `input` left-padded with `character` to at least `width` bytes, or +/// a copy of `input` when it is already that long. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::pad_left("42", 5, '0') == "00042"); +/// assert(sourcemeta::core::pad_left("hello", 3, '0') == "hello"); +/// ``` +SOURCEMETA_CORE_TEXT_EXPORT +auto pad_left(const std::string_view input, const std::size_t width, + const char character) -> std::string; + /// @ingroup text /// /// Return the prefix of `input` up to (but excluding) the first occurrence @@ -317,6 +365,39 @@ auto join_to(std::ostream &stream, const Range &items, } } +/// @ingroup text +/// +/// Decode a hexadecimal string into its raw bytes, returning no value when +/// the input contains a character outside the hexadecimal alphabet, or has an +/// odd length unless `allow_odd_length` is set, in which case a leading zero +/// nibble is assumed. Both letter cases are accepted. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto bytes{sourcemeta::core::hex_to_bytes("666f6f")}; +/// assert(bytes.has_value()); +/// assert(bytes.value() == "foo"); +/// ``` +SOURCEMETA_CORE_TEXT_EXPORT +auto hex_to_bytes(const std::string_view input, + const bool allow_odd_length = false) + -> std::optional; + +/// @ingroup text +/// +/// Encode a byte sequence as a lowercase hexadecimal string. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::bytes_to_hex("foo") == "666f6f"); +/// ``` +SOURCEMETA_CORE_TEXT_EXPORT +auto bytes_to_hex(const std::string_view input) -> std::string; + /// @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 e7e95add..52df89d0 100644 --- a/vendor/core/src/lang/text/text.cc +++ b/vendor/core/src/lang/text/text.cc @@ -1,6 +1,7 @@ #include #include // std::size_t +#include // std::int8_t #include // std::filesystem::path #include // std::optional, std::nullopt #include // std::string @@ -25,6 +26,18 @@ auto to_ascii_uppercase(const char character) noexcept -> char { : character; } +auto hex_digit_value(const char character) noexcept -> std::int8_t { + if (character >= '0' && character <= '9') { + return static_cast(character - '0'); + } else if (character >= 'a' && character <= 'f') { + return static_cast(character - 'a' + 10); + } else if (character >= 'A' && character <= 'F') { + return static_cast(character - 'A' + 10); + } else { + return -1; + } +} + } // namespace namespace sourcemeta::core { @@ -93,6 +106,33 @@ auto trim(const std::string_view input) noexcept -> std::string_view { return result; } +auto strip_left(const std::string_view input, const char character) noexcept + -> std::string_view { + std::string_view result{input}; + while (!result.empty() && result.front() == character) { + result.remove_prefix(1); + } + return result; +} + +auto strip_right(const std::string_view input, const char character) noexcept + -> std::string_view { + std::string_view result{input}; + while (!result.empty() && result.back() == character) { + result.remove_suffix(1); + } + return result; +} + +auto pad_left(const std::string_view input, const std::size_t width, + const char character) -> std::string { + if (input.size() >= width) { + return std::string{input}; + } + + return std::string(width - input.size(), character) + std::string{input}; +} + auto take_until(const std::string_view input, const char marker) noexcept -> std::string_view { const auto position{input.find(marker)}; @@ -153,4 +193,51 @@ auto remove_suffix_ignore_case(const std::string_view input, return result; } +auto hex_to_bytes(const std::string_view input, const bool allow_odd_length) + -> std::optional { + const auto odd_length{input.size() % 2 != 0}; + if (odd_length && !allow_odd_length) { + return std::nullopt; + } + + std::string result; + result.reserve(input.size() / 2 + 1); + + std::size_t index{0}; + if (odd_length) { + const auto nibble{hex_digit_value(input[0])}; + if (nibble < 0) { + return std::nullopt; + } + + result.push_back(static_cast(nibble)); + index = 1; + } + + for (; index < input.size(); index += 2) { + const auto high{hex_digit_value(input[index])}; + const auto low{hex_digit_value(input[index + 1])}; + if (high < 0 || low < 0) { + return std::nullopt; + } + + result.push_back(static_cast((high << 4) | low)); + } + + return result; +} + +auto bytes_to_hex(const std::string_view input) -> std::string { + static constexpr std::string_view digits{"0123456789abcdef"}; + std::string result; + result.reserve(input.size() * 2); + for (const auto character : input) { + const auto byte{static_cast(character)}; + result.push_back(digits[byte >> 4u]); + result.push_back(digits[byte & 0x0fu]); + } + + return result; +} + } // namespace sourcemeta::core