diff --git a/DEPENDENCIES b/DEPENDENCIES index 2c078a71..7947a872 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,4 +1,4 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core df8f2970ccf85a3a3f01e004ac436ff916f8c52a -blaze https://github.com/sourcemeta/blaze 7b214cff6d575831c16a2ce33f55d97c02eb6338 +core https://github.com/sourcemeta/core bb1c78e8fa148a2ece951bb776798a43fe328821 +blaze https://github.com/sourcemeta/blaze 04832d45bf4327d4ec874fa67f339797cd49b375 bootstrap https://github.com/twbs/bootstrap 1a6fdfae6be09b09eaced8f0e442ca6f7680a61e diff --git a/vendor/blaze/DEPENDENCIES b/vendor/blaze/DEPENDENCIES index 2bf88c51..a7156988 100644 --- a/vendor/blaze/DEPENDENCIES +++ b/vendor/blaze/DEPENDENCIES @@ -1,5 +1,5 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core df8f2970ccf85a3a3f01e004ac436ff916f8c52a +core https://github.com/sourcemeta/core bb1c78e8fa148a2ece951bb776798a43fe328821 jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite 60755c1097769e313fae3ec4d63bcc9d49b5d2d5 jsonschema-2020-12 https://github.com/json-schema-org/json-schema-spec 769daad75a9553562333a8937a187741cb708c72 jsonschema-2019-09 https://github.com/json-schema-org/json-schema-spec 41014ea723120ce70b314d72f863c6929d9f3cfd diff --git a/vendor/blaze/schemas/canonical-draft3.json b/vendor/blaze/schemas/canonical-draft3.json index 9089bf0c..f9a35075 100644 --- a/vendor/blaze/schemas/canonical-draft3.json +++ b/vendor/blaze/schemas/canonical-draft3.json @@ -137,9 +137,6 @@ }, "format": { "type": "string" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -169,9 +166,6 @@ }, "maximum": { "type": "number" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -207,9 +201,6 @@ }, "exclusiveMaximum": { "type": "boolean" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -256,9 +247,6 @@ "type": "boolean" } ] - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -292,9 +280,6 @@ }, "uniqueItems": { "type": "boolean" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -348,9 +333,6 @@ }, "uniqueItems": { "type": "boolean" - }, - "required": { - "type": "boolean" } }, "unevaluatedProperties": false @@ -397,6 +379,9 @@ "items": { "$ref": "#/$defs/schema" } + }, + "required": { + "type": "boolean" } }, "unevaluatedProperties": false @@ -416,6 +401,7 @@ "properties": { "disallow": { "type": "array", + "maxItems": 1, "minItems": 1, "items": { "$ref": "#/$defs/schema" diff --git a/vendor/blaze/src/alterschema/CMakeLists.txt b/vendor/blaze/src/alterschema/CMakeLists.txt index 37c5b5a1..6fd7ad96 100644 --- a/vendor/blaze/src/alterschema/CMakeLists.txt +++ b/vendor/blaze/src/alterschema/CMakeLists.txt @@ -12,13 +12,18 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema canonicalizer/dependent_schemas_to_any_of.h canonicalizer/deprecated_false_drop.h canonicalizer/draft3_type_any.h + canonicalizer/disallow_array_to_extends.h + canonicalizer/disallow_extends_to_type.h canonicalizer/disallow_to_array_of_schemas.h + canonicalizer/disallow_type_union_to_extends.h canonicalizer/divisible_by_implicit.h + canonicalizer/duplicate_disallow_entries.h canonicalizer/empty_definitions_drop.h canonicalizer/empty_defs_drop.h canonicalizer/empty_dependencies_drop.h canonicalizer/empty_dependent_required_drop.h canonicalizer/empty_dependent_schemas_drop.h + canonicalizer/empty_disallow_drop.h canonicalizer/enum_drop_redundant_validation.h canonicalizer/enum_filter_by_type.h canonicalizer/exclusive_maximum_boolean_integer_fold.h @@ -43,6 +48,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema canonicalizer/optional_property_implicit.h canonicalizer/recursive_anchor_false_drop.h canonicalizer/required_property_implicit.h + canonicalizer/required_to_extends.h canonicalizer/single_branch_allof.h canonicalizer/single_branch_anyof.h canonicalizer/single_branch_oneof.h @@ -50,6 +56,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema canonicalizer/type_boolean_as_enum.h canonicalizer/type_inherit_in_place.h canonicalizer/type_null_as_enum.h + canonicalizer/type_union_distribute_keywords.h canonicalizer/type_union_implicit.h canonicalizer/type_union_to_schemas.h canonicalizer/type_with_applicator_to_allof.h diff --git a/vendor/blaze/src/alterschema/alterschema.cc b/vendor/blaze/src/alterschema/alterschema.cc index 4e511019..efadc22f 100644 --- a/vendor/blaze/src/alterschema/alterschema.cc +++ b/vendor/blaze/src/alterschema/alterschema.cc @@ -118,14 +118,19 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "canonicalizer/dependent_required_to_any_of.h" #include "canonicalizer/dependent_schemas_to_any_of.h" #include "canonicalizer/deprecated_false_drop.h" +#include "canonicalizer/disallow_array_to_extends.h" +#include "canonicalizer/disallow_extends_to_type.h" #include "canonicalizer/disallow_to_array_of_schemas.h" +#include "canonicalizer/disallow_type_union_to_extends.h" #include "canonicalizer/divisible_by_implicit.h" #include "canonicalizer/draft3_type_any.h" +#include "canonicalizer/duplicate_disallow_entries.h" #include "canonicalizer/empty_definitions_drop.h" #include "canonicalizer/empty_defs_drop.h" #include "canonicalizer/empty_dependencies_drop.h" #include "canonicalizer/empty_dependent_required_drop.h" #include "canonicalizer/empty_dependent_schemas_drop.h" +#include "canonicalizer/empty_disallow_drop.h" #include "canonicalizer/enum_drop_redundant_validation.h" #include "canonicalizer/enum_filter_by_type.h" #include "canonicalizer/exclusive_maximum_boolean_integer_fold.h" @@ -151,6 +156,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "canonicalizer/optional_property_implicit.h" #include "canonicalizer/recursive_anchor_false_drop.h" #include "canonicalizer/required_property_implicit.h" +#include "canonicalizer/required_to_extends.h" #include "canonicalizer/single_branch_allof.h" #include "canonicalizer/single_branch_anyof.h" #include "canonicalizer/single_branch_oneof.h" @@ -158,6 +164,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "canonicalizer/type_boolean_as_enum.h" #include "canonicalizer/type_inherit_in_place.h" #include "canonicalizer/type_null_as_enum.h" +#include "canonicalizer/type_union_distribute_keywords.h" #include "canonicalizer/type_union_implicit.h" #include "canonicalizer/type_union_to_schemas.h" #include "canonicalizer/type_with_applicator_to_allof.h" @@ -511,6 +518,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); @@ -523,9 +531,15 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); + bundle.add(); + bundle.add(); + bundle.add(); + bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); diff --git a/vendor/blaze/src/alterschema/canonicalizer/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/disallow_array_to_extends.h b/vendor/blaze/src/alterschema/canonicalizer/disallow_array_to_extends.h new file mode 100644 index 00000000..293b9bd5 --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/disallow_array_to_extends.h @@ -0,0 +1,85 @@ +class DisallowArrayToExtends final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DisallowArrayToExtends() + : SchemaTransformRule{ + "disallow_array_to_extends", + "A multi-way `disallow` is the conjunction of single negations: " + "each element becomes its own single-element `disallow` in an " + "`extends` branch"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->size() > 1); + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto branches{JSON::make_array()}; + for (const auto &element : schema.at("disallow").as_array()) { + auto negation{JSON::make_array()}; + negation.push_back(element); + auto branch{JSON::make_object()}; + branch.assign("disallow", std::move(negation)); + branches.push_back(std::move(branch)); + } + + schema.erase("disallow"); + + if (schema.defines("extends") && schema.at("extends").is_array()) { + this->extends_start_ = schema.at("extends").size(); + for (auto &branch : branches.as_array()) { + schema.at("extends").push_back(std::move(branch)); + } + } else if (schema.defines("extends")) { + auto extends{JSON::make_array()}; + extends.push_back(schema.at("extends")); + this->extends_start_ = extends.size(); + for (auto &branch : branches.as_array()) { + extends.push_back(std::move(branch)); + } + schema.assign("extends", std::move(extends)); + } else { + this->extends_start_ = 0; + schema.assign("extends", std::move(branches)); + } + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + const auto disallow_prefix{current.concat({"disallow"})}; + if (!target.starts_with(disallow_prefix)) { + return target; + } + + const auto relative{target.resolve_from(disallow_prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + + const auto index{relative.at(0).to_index()}; + return target.rebase( + current.concat({"disallow", index}), + current.concat( + {"extends", this->extends_start_ + index, "disallow", 0})); + } + +private: + mutable std::size_t extends_start_{0}; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h b/vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h new file mode 100644 index 00000000..7cb88c1b --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/disallow_extends_to_type.h @@ -0,0 +1,109 @@ +class DisallowExtendsToType final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DisallowExtendsToType() + : SchemaTransformRule{ + "disallow_extends_to_type", + "Negating a conjunction is the disjunction of the negations: an " + "`extends` under `disallow` becomes a `type` union where each " + "branch is its own single negation"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->size() == 1); + + const auto &element{disallow->at(0)}; + ONLY_CONTINUE_IF(element.is_object() && element.defines("extends") && + element.at("extends").is_array() && + !element.at("extends").empty()); + + // Only a pure negation can be distributed: the schema must assert nothing + // besides `disallow` (otherwise the new `type` would clobber a sibling + // constraint), and the negated schema must assert nothing besides `extends` + // (otherwise those conjuncts would be silently dropped) + ONLY_CONTINUE_IF( + wraps_single_constraint(schema, "disallow", walker, vocabularies) && + wraps_single_constraint(element, "extends", walker, vocabularies)); + + // The conjuncts relocate to distinct `type` branches (handled by + // `rereference`), but the wrapper schema itself is dissolved rather than + // moved, so a reference straight at it has no new home: bail in that case + static const JSON::String DISALLOW{"disallow"}; + auto wrapper_pointer{location.pointer}; + wrapper_pointer.push_back(std::cref(DISALLOW)); + wrapper_pointer.push_back(static_cast(0)); + ONLY_CONTINUE_IF(!frame.has_references_to(wrapper_pointer)); + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto branches{JSON::make_array()}; + for (auto &branch : schema.at("disallow").at(0).at("extends").as_array()) { + auto negation{JSON::make_array()}; + negation.push_back(std::move(branch)); + auto element{JSON::make_object()}; + element.assign("disallow", std::move(negation)); + branches.push_back(std::move(element)); + } + + schema.erase("disallow"); + schema.assign("type", std::move(branches)); + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + const auto extends_prefix{current.concat({"disallow", 0, "extends"})}; + if (!target.starts_with(extends_prefix)) { + return target; + } + + const auto relative{target.resolve_from(extends_prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + + const auto index{relative.at(0).to_index()}; + return target.rebase(extends_prefix.concat({index}), + current.concat({"type", index, "disallow", 0})); + } + +private: + static auto wraps_single_constraint( + const sourcemeta::core::JSON &schema, const std::string_view keyword, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::Vocabularies &vocabularies) -> bool { + for (const auto &entry : schema.as_object()) { + if (entry.first == keyword) { + continue; + } + + const auto type{walker(entry.first, vocabularies).type}; + if (type != SchemaKeywordType::Annotation && + type != SchemaKeywordType::Comment && + type != SchemaKeywordType::Other && + type != SchemaKeywordType::Unknown && + type != SchemaKeywordType::LocationMembers) { + return false; + } + } + + return true; + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h b/vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h new file mode 100644 index 00000000..6763e68d --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/disallow_type_union_to_extends.h @@ -0,0 +1,109 @@ +class DisallowTypeUnionToExtends final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DisallowTypeUnionToExtends() + : SchemaTransformRule{ + "disallow_type_union_to_extends", + "Negating a disjunction is the conjunction of the negations: a " + "`type` union under `disallow` becomes an `extends` where each " + "branch is its own single negation"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->size() == 1); + + const auto &element{disallow->at(0)}; + ONLY_CONTINUE_IF(element.is_object() && element.defines("type") && + element.at("type").is_array() && + !element.at("type").empty()); + + // Only a pure negation can be distributed: the schema must assert nothing + // besides `disallow` (otherwise the new `extends` would clobber a sibling + // constraint), and the negated schema must assert nothing besides its + // `type` union (otherwise those conjuncts would be silently dropped) + ONLY_CONTINUE_IF( + wraps_single_constraint(schema, "disallow", walker, vocabularies) && + wraps_single_constraint(element, "type", walker, vocabularies)); + + // The union members relocate to distinct `extends` branches (handled by + // `rereference`), but the wrapper schema itself is dissolved rather than + // moved, so a reference straight at it has no new home: bail in that case + static const JSON::String DISALLOW{"disallow"}; + auto wrapper_pointer{location.pointer}; + wrapper_pointer.push_back(std::cref(DISALLOW)); + wrapper_pointer.push_back(static_cast(0)); + ONLY_CONTINUE_IF(!frame.has_references_to(wrapper_pointer)); + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + auto branches{JSON::make_array()}; + for (auto &member : schema.at("disallow").at(0).at("type").as_array()) { + auto negation{JSON::make_array()}; + negation.push_back(std::move(member)); + auto branch{JSON::make_object()}; + branch.assign("disallow", std::move(negation)); + branches.push_back(std::move(branch)); + } + + schema.erase("disallow"); + schema.assign("extends", std::move(branches)); + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + const auto type_prefix{current.concat({"disallow", 0, "type"})}; + if (!target.starts_with(type_prefix)) { + return target; + } + + const auto relative{target.resolve_from(type_prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + + const auto index{relative.at(0).to_index()}; + return target.rebase(type_prefix.concat({index}), + current.concat({"extends", index, "disallow", 0})); + } + +private: + static auto wraps_single_constraint( + const sourcemeta::core::JSON &schema, const std::string_view keyword, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::Vocabularies &vocabularies) -> bool { + for (const auto &entry : schema.as_object()) { + if (entry.first == keyword) { + continue; + } + + const auto type{walker(entry.first, vocabularies).type}; + if (type != SchemaKeywordType::Annotation && + type != SchemaKeywordType::Comment && + type != SchemaKeywordType::Other && + type != SchemaKeywordType::Unknown && + type != SchemaKeywordType::LocationMembers) { + return false; + } + } + + return true; + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h b/vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h new file mode 100644 index 00000000..aecdcda2 --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/duplicate_disallow_entries.h @@ -0,0 +1,58 @@ +class DuplicateDisallowEntries final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DuplicateDisallowEntries() + : SchemaTransformRule{ + "duplicate_disallow_entries", + "Setting duplicate subschemas in `disallow` is redundant, as " + "negating the same subschema more than once is guaranteed to not " + "affect the validation result"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &frame, + const sourcemeta::blaze::SchemaFrame::Location &location, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && !disallow->unique()); + + // Compacting the array would shift the index of every entry that follows a + // removed duplicate, so a reference into `disallow` could silently end up + // pointing at a different subschema. Leave such cases untouched and let + // `DisallowArrayToExtends` split them instead, which preserves every index + // as its own `extends` branch + const std::string keyword{"disallow"}; + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(keyword)})); + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + const auto &original{schema.at("disallow")}; + + std::unordered_set, + HashJSON>, + EqualJSON>> + seen; + auto result{JSON::make_array()}; + + for (const auto &element : original.as_array()) { + if (seen.emplace(std::cref(element)).second) { + result.push_back(element); + } + } + + schema.assign("disallow", std::move(result)); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h b/vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h new file mode 100644 index 00000000..429e85c7 --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/empty_disallow_drop.h @@ -0,0 +1,29 @@ +class EmptyDisallowDrop final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + EmptyDisallowDrop() : SchemaTransformRule{"empty_disallow_drop", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->empty()); + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.erase("disallow"); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h b/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h index 8a4d9b51..b9233504 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h +++ b/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h @@ -63,6 +63,27 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { continue; } + // In Draft 3 and older, `required` and `optional` are property-presence + // flags read by the parent object validator, not value assertions that an + // `enum` could make redundant + if (entry.first == "required" && + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})) { + continue; + } + + if (entry.first == "optional" && + vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_0, + Vocabularies::Known::JSON_Schema_Draft_0_Hyper, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_1_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_2_Hyper})) { + continue; + } + if (entry.second.is_boolean() && entry.second.to_boolean()) { if (!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(entry.first)})) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h b/vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h new file mode 100644 index 00000000..44e9845e --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/required_to_extends.h @@ -0,0 +1,99 @@ +class RequiredToExtends final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + RequiredToExtends() + : SchemaTransformRule{ + "required_to_extends", + "In Draft 3 canonical form, `required` is only ever a sibling of " + "`extends`; its other siblings are wrapped into an `extends` " + "branch"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_boolean()); + + for (const auto &entry : schema.as_object()) { + if (!stays_at_top(entry.first)) { + return true; + } + } + + return false; + } + + auto transform(JSON &schema, const Result &) const -> void override { + this->wrapped_keywords_.clear(); + for (const auto &entry : schema.as_object()) { + if (!stays_at_top(entry.first)) { + this->wrapped_keywords_.push_back(entry.first); + } + } + + auto branch{JSON::make_object()}; + for (const auto &keyword : this->wrapped_keywords_) { + branch.assign(keyword, schema.at(keyword)); + } + + for (const auto &keyword : this->wrapped_keywords_) { + schema.erase(keyword); + } + + if (schema.defines("extends") && schema.at("extends").is_array()) { + this->branch_index_ = schema.at("extends").size(); + schema.at("extends").push_back(std::move(branch)); + } else if (schema.defines("extends")) { + // Draft 3 allows `extends` to be a single schema; preserve it as the + // first branch of the new array + auto extends{JSON::make_array()}; + extends.push_back(schema.at("extends")); + this->branch_index_ = extends.size(); + extends.push_back(std::move(branch)); + schema.assign("extends", std::move(extends)); + } else { + this->branch_index_ = 0; + auto extends{JSON::make_array()}; + extends.push_back(std::move(branch)); + schema.assign("extends", std::move(extends)); + } + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + for (const auto &keyword : this->wrapped_keywords_) { + const auto keyword_prefix{current.concat({keyword})}; + if (target.starts_with(keyword_prefix)) { + return target.rebase( + keyword_prefix, + current.concat({"extends", this->branch_index_, keyword})); + } + } + + return target; + } + +private: + static auto stays_at_top(const sourcemeta::core::JSON::String &keyword) + -> bool { + return keyword == "required" || keyword == "extends" || + keyword == "$schema" || keyword == "id" || keyword == "$ref"; + } + + mutable std::vector wrapped_keywords_; + mutable std::size_t branch_index_{0}; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_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/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h b/vendor/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h new file mode 100644 index 00000000..04ba7d5e --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/type_union_distribute_keywords.h @@ -0,0 +1,207 @@ +class TypeUnionDistributeKeywords final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + TypeUnionDistributeKeywords() + : SchemaTransformRule{ + "type_union_distribute_keywords", + "A type-specific keyword sibling to a `type` union belongs inside " + "the branch of the type that it applies to"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::blaze::Vocabularies &vocabularies, + const sourcemeta::blaze::SchemaFrame &, + const sourcemeta::blaze::SchemaFrame::Location &, + const sourcemeta::blaze::SchemaWalker &walker, + const sourcemeta::blaze::SchemaResolver &, const bool) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_array() && !type->empty()); + for (const auto &branch : type->as_array()) { + ONLY_CONTINUE_IF(branch.is_object()); + } + + this->moves_.clear(); + this->wrap_keywords_.clear(); + this->wrap_ = false; + std::vector movable; + for (const auto &entry : schema.as_object()) { + // `required` is a property-presence flag, not a value assertion, so it + // is never pushed into a branch + if (entry.first == "type" || entry.first == "required") { + continue; + } + + const auto &metadata{walker(entry.first, vocabularies)}; + if (metadata.type == sourcemeta::blaze::SchemaKeywordType::Reference) { + continue; + } + + // A keyword that applies to every type carries no type-specific + // information to push down into a branch + if (metadata.instances.none()) { + continue; + } + + movable.push_back(entry.first); + + std::vector targets; + bool has_match{false}; + bool conflict{false}; + for (std::size_t index = 0; index < type->size(); ++index) { + const auto branch_types{branch_type_set(type->at(index))}; + if ((branch_types & metadata.instances).none()) { + continue; + } + + has_match = true; + // A matching branch already constrains this keyword, so distributing + // and erasing the sibling could drop the top-level bound from that + // branch. Wrap instead so nothing is weakened. + if (type->at(index).defines(entry.first)) { + conflict = true; + break; + } + + targets.push_back(index); + } + + if (!has_match || conflict) { + this->wrap_ = true; + } else { + this->moves_.emplace_back(entry.first, std::move(targets)); + } + } + + ONLY_CONTINUE_IF(!movable.empty()); + if (this->wrap_) { + this->moves_.clear(); + this->wrap_keywords_ = std::move(movable); + } + + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + if (this->wrap_) { + auto union_branch{JSON::make_object()}; + union_branch.assign("type", schema.at("type")); + auto sibling_branch{JSON::make_object()}; + for (const auto &keyword : this->wrap_keywords_) { + sibling_branch.assign(keyword, schema.at(keyword)); + } + + schema.erase("type"); + for (const auto &keyword : this->wrap_keywords_) { + schema.erase(keyword); + } + + if (schema.defines("extends") && schema.at("extends").is_array()) { + this->type_index_ = schema.at("extends").size(); + schema.at("extends").push_back(std::move(union_branch)); + this->sibling_index_ = schema.at("extends").size(); + schema.at("extends").push_back(std::move(sibling_branch)); + } else { + auto extends{JSON::make_array()}; + this->type_index_ = 0; + extends.push_back(std::move(union_branch)); + this->sibling_index_ = 1; + extends.push_back(std::move(sibling_branch)); + schema.assign("extends", std::move(extends)); + } + + return; + } + + for (const auto &entry : this->moves_) { + const auto value{schema.at(entry.first)}; + auto &type{schema.at("type")}; + for (const auto index : entry.second) { + type.at(index).assign(entry.first, value); + } + } + + for (const auto &entry : this->moves_) { + schema.erase(entry.first); + } + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + if (this->wrap_) { + const auto type_prefix{current.concat({"type"})}; + if (target.starts_with(type_prefix)) { + return target.rebase( + type_prefix, + current.concat({"extends", this->type_index_, "type"})); + } + + for (const auto &keyword : this->wrap_keywords_) { + const auto keyword_prefix{current.concat({keyword})}; + if (target.starts_with(keyword_prefix)) { + return target.rebase( + keyword_prefix, + current.concat({"extends", this->sibling_index_, keyword})); + } + } + + return target; + } + + for (const auto &entry : this->moves_) { + if (entry.second.empty()) { + continue; + } + + const auto keyword_prefix{current.concat({entry.first})}; + if (target.starts_with(keyword_prefix)) { + return target.rebase( + keyword_prefix, + current.concat({"type", entry.second.front(), entry.first})); + } + } + + return target; + } + +private: + static auto branch_type_set(const sourcemeta::core::JSON &branch) + -> sourcemeta::core::JSON::TypeSet { + if (!branch.is_object()) { + return {}; + } + + const auto *type{branch.try_at("type")}; + if (type && (type->is_string() || type->is_array())) { + return parse_schema_type(*type); + } + + const auto *enum_value{branch.try_at("enum")}; + if (enum_value && enum_value->is_array()) { + sourcemeta::core::JSON::TypeSet result; + for (const auto &value : enum_value->as_array()) { + result.set(std::to_underlying(value.type())); + } + return result; + } + + return {}; + } + + mutable std::vector< + std::pair>> + moves_; + mutable std::vector wrap_keywords_; + mutable bool wrap_{false}; + mutable std::size_t type_index_{0}; + mutable std::size_t sibling_index_{0}; +}; diff --git a/vendor/blaze/src/bundle/bundle.cc b/vendor/blaze/src/bundle/bundle.cc index 74ee2c58..d7d633d0 100644 --- a/vendor/blaze/src/bundle/bundle.cc +++ b/vendor/blaze/src/bundle/bundle.cc @@ -100,9 +100,16 @@ auto dependencies_internal( } callback(origin, pointer, identifier, remote.value()); + visited.emplace(identifier); + + // Official schemas can only reference other official schemas, so + // recursing into them can never surface further dependencies + if (sourcemeta::blaze::is_official_schema(identifier)) { + return; + } + found.emplace_back(std::move(remote).value(), sourcemeta::core::JSON::String{identifier}); - visited.emplace(identifier); }); for (const auto &entry : found) { diff --git a/vendor/blaze/src/bundle/include/sourcemeta/blaze/bundle.h b/vendor/blaze/src/bundle/include/sourcemeta/blaze/bundle.h index b5160e79..e7d90c8f 100644 --- a/vendor/blaze/src/bundle/include/sourcemeta/blaze/bundle.h +++ b/vendor/blaze/src/bundle/include/sourcemeta/blaze/bundle.h @@ -52,7 +52,8 @@ enum class BundleMode : std::uint8_t { /// @ingroup bundle /// /// This function recursively traverses and reports the external references in a -/// schema. For example: +/// schema. References to official schemas are reported but not traversed into, +/// as official schemas can only reference other official schemas. For example: /// /// ```cpp /// #include diff --git a/vendor/blaze/src/compiler/compile.cc b/vendor/blaze/src/compiler/compile.cc index 7cf3354a..2e3a3e16 100644 --- a/vendor/blaze/src/compiler/compile.cc +++ b/vendor/blaze/src/compiler/compile.cc @@ -21,8 +21,7 @@ namespace { auto compile_subschema(const sourcemeta::blaze::Context &context, const sourcemeta::blaze::SchemaContext &schema_context, - const sourcemeta::blaze::DynamicContext &dynamic_context, - const std::string_view default_dialect) + const sourcemeta::blaze::DynamicContext &dynamic_context) -> sourcemeta::blaze::Instructions { using namespace sourcemeta::blaze; assert(is_schema(schema_context.schema)); @@ -45,8 +44,8 @@ auto compile_subschema(const sourcemeta::blaze::Context &context, Instructions steps; for (const auto &entry : sourcemeta::blaze::SchemaKeywordIterator{ - schema_context.schema, context.walker, context.resolver, - default_dialect}) { + schema_context.schema, context.walker, + schema_context.vocabularies}) { assert(entry.pointer.back().is_property()); const auto &keyword{entry.pointer.back().to_property()}; // Bases must not contain fragments @@ -381,8 +380,8 @@ auto compile(const sourcemeta::core::JSON &schema, } auto subschema{sourcemeta::core::get(context.root, entry.pointer)}; - auto nested_vocabularies{sourcemeta::blaze::vocabularies( - subschema, context.resolver, entry.dialect)}; + auto nested_vocabularies{ + context.frame.vocabularies(entry, context.resolver)}; const auto nested_relative_pointer{ entry.pointer.slice(entry.relative_pointer)}; const sourcemeta::core::URI nested_base{entry.base}; @@ -496,15 +495,13 @@ auto compile(const Context &context, const SchemaContext &schema_context, context, {.relative_pointer = new_relative_pointer, .schema = new_schema, - .vocabularies = - vocabularies(new_schema, context.resolver, entry.dialect), + .vocabularies = context.frame.vocabularies(entry, context.resolver), .base = new_base, .is_property_name = schema_context.is_property_name}, {.keyword = dynamic_context.keyword, .base_schema_location = destination_pointer, .base_instance_location = - dynamic_context.base_instance_location.concat(instance_suffix)}, - entry.dialect); + dynamic_context.base_instance_location.concat(instance_suffix)}); } } // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/compiler/compile_helpers.h b/vendor/blaze/src/compiler/compile_helpers.h index 1a93740b..a36190e9 100644 --- a/vendor/blaze/src/compiler/compile_helpers.h +++ b/vendor/blaze/src/compiler/compile_helpers.h @@ -9,6 +9,7 @@ #include // assert #include // std::cref #include // std::distance +#include // std::optional #include // std::regex, std::regex_match, std::smatch #include // std::declval, std::move @@ -186,13 +187,27 @@ inline auto static_frame_entry(const Context &context, return context.frame.locations().at({type, current}); } -inline auto walk_subschemas(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context) -> auto { +// Whether the current keyword value, as a schema, contains any nested +// subschema. Note that while the schema of the schema context of a keyword +// compiler is the parent subschema, its relative pointer already targets +// the keyword value, so the frame entry we look up corresponds to the +// keyword value and not to the parent subschema +inline auto defines_nested_subschemas(const Context &context, + const SchemaContext &schema_context) + -> bool { const auto &entry{static_frame_entry(context, schema_context)}; - return sourcemeta::blaze::SchemaIterator{ - schema_context.schema.at(dynamic_context.keyword), context.walker, - context.resolver, entry.dialect}; + for (const auto &location : context.frame.locations()) { + if ((location.second.type == + sourcemeta::blaze::SchemaFrame::LocationType::Subschema || + location.second.type == + sourcemeta::blaze::SchemaFrame::LocationType::Resource) && + location.second.pointer.starts_with(entry.pointer) && + location.second.pointer.size() > entry.pointer.size()) { + return true; + } + } + + return false; } // TODO: Get rid of this given the new Core regex optimisations @@ -266,8 +281,8 @@ inline auto find_adjacent(const Context &context, possible_keyword_uri})}; const auto &subschema{ sourcemeta::core::get(context.root, frame_entry.pointer)}; - const auto &subschema_vocabularies{sourcemeta::blaze::vocabularies( - subschema, context.resolver, frame_entry.dialect)}; + const auto subschema_vocabularies{ + context.frame.vocabularies(frame_entry, context.resolver)}; if (std::ranges::any_of(vocabularies, [&subschema_vocabularies](const auto &vocabulary) { @@ -394,7 +409,10 @@ inline auto required_properties(const SchemaContext &schema_context) schema_context.schema.at("properties").is_object()) { for (const auto &entry : schema_context.schema.at("properties").as_object()) { + // In Draft 3, keywords sibling to `$ref` are never evaluated, so a + // `required` flag next to a `$ref` does not make the property mandatory if (entry.second.is_object() && entry.second.defines("required") && + !entry.second.defines("$ref") && entry.second.at("required").is_boolean() && entry.second.at("required").to_boolean()) { result.insert(entry.first); diff --git a/vendor/blaze/src/compiler/default_compiler_draft4.h b/vendor/blaze/src/compiler/default_compiler_draft4.h index 08f9a551..995c3f96 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft4.h +++ b/vendor/blaze/src/compiler/default_compiler_draft4.h @@ -182,15 +182,7 @@ auto compiler_draft4_applicator_not(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - std::size_t subschemas{0}; - for (const auto &subschema : - walk_subschemas(context, schema_context, dynamic_context)) { - if (subschema.pointer.empty()) { - continue; - } - - subschemas += 1; - } + const auto subschemas{defines_nested_subschemas(context, schema_context)}; Instructions children{compile(context, schema_context, relative_dynamic_context(), @@ -208,7 +200,7 @@ auto compiler_draft4_applicator_not(const Context &context, // evaluation if we really need it. If the "not" subschema // does not define applicators, then that's an easy case // we can skip - if (subschemas > 0 && + if (subschemas && (requires_evaluation(context, schema_context) || track_items)) { return {make(sourcemeta::blaze::InstructionIndex::LogicalNotEvaluate, context, schema_context, dynamic_context, ValueNone{}, diff --git a/vendor/blaze/src/compiler/unevaluated.cc b/vendor/blaze/src/compiler/unevaluated.cc index 1cc8f61a..66d2f747 100644 --- a/vendor/blaze/src/compiler/unevaluated.cc +++ b/vendor/blaze/src/compiler/unevaluated.cc @@ -22,8 +22,7 @@ auto find_adjacent_dependencies( return; } - const auto subschema_vocabularies{ - vocabularies(subschema, resolver, entry.dialect)}; + const auto subschema_vocabularies{frame.vocabularies(entry, resolver)}; for (const auto &property : subschema.as_object()) { if (property.first == current && entry.pointer == root.pointer) { diff --git a/vendor/blaze/src/foundation/foundation.cc b/vendor/blaze/src/foundation/foundation.cc index 438ffba3..e3f54b0a 100644 --- a/vendor/blaze/src/foundation/foundation.cc +++ b/vendor/blaze/src/foundation/foundation.cc @@ -265,6 +265,102 @@ auto sourcemeta::blaze::dialect(const sourcemeta::core::JSON &schema, return dialect_value.to_string(); } +// A meta-schema that is not known to the resolver may still be embedded in +// the document itself. Across every official base dialect, the only +// containers that can hold embedded resources are `$defs` and `definitions`, +// which no custom dialect can redefine away. A candidate only counts if its +// entire meta-schema chain terminates at an official base dialect and every +// embedded link declares its identifier and sits in a container in a way +// that is valid for such base dialect +auto sourcemeta::blaze::metaschema_try_embedded( + const sourcemeta::core::JSON &schema, const std::string_view identifier, + const SchemaResolver &resolver) -> const sourcemeta::core::JSON * { + // Relative or invalid meta-schema references are not acceptable + // according to the JSON Schema specifications + if (!sourcemeta::core::URI::is_uri(identifier)) { + return nullptr; + } + + const auto candidate{ + sourcemeta::blaze::embedded_metaschema_candidate(schema, identifier)}; + if (!candidate.first) { + return nullptr; + } + + std::unordered_set visited; + std::vector links{ + {.schema = candidate.first, + .identifier = identifier, + .container = candidate.second}}; + // Chain links that the resolver knows about are returned by value, so we + // keep them alive while we walk the chain, in a container that never + // relocates its elements, as we hold views into them + std::deque resolved; + const auto *current{candidate.first}; + std::string_view current_identifier{identifier}; + std::optional terminal; + + while (true) { + // The meta-schema is present, but its chain can never terminate at an + // official base dialect, just like a self-descriptive or cyclic + // meta-schema that the resolver knows about + if (!visited.emplace(current_identifier).second) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + if (!current->is_object()) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + const auto *metaschema_dialect{current->try_at("$schema")}; + if (!metaschema_dialect || !metaschema_dialect->is_string()) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + const auto &dialect_uri{metaschema_dialect->to_string()}; + const auto known{sourcemeta::blaze::to_base_dialect(dialect_uri)}; + if (known.has_value()) { + terminal = known; + break; + } + + auto remote{resolver(dialect_uri)}; + if (remote.has_value()) { + resolved.push_back(std::move(remote).value()); + current = &resolved.back(); + current_identifier = dialect_uri; + continue; + } + + if (!sourcemeta::core::URI::is_uri(dialect_uri)) { + return nullptr; + } + + const auto next{ + sourcemeta::blaze::embedded_metaschema_candidate(schema, dialect_uri)}; + if (!next.first) { + return nullptr; + } + + links.push_back({.schema = next.first, + .identifier = dialect_uri, + .container = next.second}); + current = next.first; + current_identifier = dialect_uri; + } + + assert(terminal.has_value()); + for (const auto &link : links) { + if (!sourcemeta::blaze::embedded_metaschema_link_valid( + *(link.schema), link.identifier, link.container, + terminal.value())) { + return nullptr; + } + } + + return candidate.first; +} + auto sourcemeta::blaze::metaschema( const sourcemeta::core::JSON &schema, const sourcemeta::blaze::SchemaResolver &resolver, @@ -275,6 +371,15 @@ auto sourcemeta::blaze::metaschema( throw sourcemeta::blaze::SchemaUnknownDialectError(); } + // A meta-schema that is embedded in the schema itself takes precedence + // over what the resolver knows about, as the schema pins the exact + // meta-schema it is described by + const auto *embedded{sourcemeta::blaze::metaschema_try_embedded( + schema, effective_dialect, resolver)}; + if (embedded) { + return *embedded; + } + const auto maybe_metaschema{resolver(effective_dialect)}; if (!maybe_metaschema.has_value()) { // Relative meta-schema references are invalid according to the @@ -297,7 +402,8 @@ base_dialect_with_visited(const sourcemeta::core::JSON &schema, const sourcemeta::blaze::SchemaResolver &resolver, std::string_view default_dialect, std::unordered_set &visited, - const bool allow_dialect_override) + const bool allow_dialect_override, + const sourcemeta::core::JSON &document) -> std::optional { assert(sourcemeta::blaze::is_schema(schema)); const std::string_view effective_dialect{sourcemeta::blaze::dialect( @@ -320,6 +426,22 @@ base_dialect_with_visited(const sourcemeta::core::JSON &schema, throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); } + // A meta-schema that is embedded in the original document itself takes + // precedence over what the resolver knows about, as the document pins + // the exact meta-schema it is described by + const auto *embedded{sourcemeta::blaze::metaschema_try_embedded( + document, effective_dialect, resolver)}; + if (embedded) { + const std::string_view embedded_dialect{sourcemeta::blaze::dialect( + *embedded, effective_dialect, allow_dialect_override)}; + if (embedded_dialect == effective_dialect) { + throw sourcemeta::blaze::SchemaUnknownBaseDialectError(); + } + + return base_dialect_with_visited(*embedded, resolver, effective_dialect, + visited, allow_dialect_override, document); + } + // Otherwise, traverse the metaschema hierarchy up const std::optional metaschema{ resolver(effective_dialect)}; @@ -353,7 +475,7 @@ base_dialect_with_visited(const sourcemeta::core::JSON &schema, return base_dialect_with_visited(metaschema.value(), resolver, effective_dialect, visited, - allow_dialect_override); + allow_dialect_override, document); } auto sourcemeta::blaze::base_dialect( @@ -363,7 +485,7 @@ auto sourcemeta::blaze::base_dialect( -> std::optional { std::unordered_set visited; return base_dialect_with_visited(schema, resolver, default_dialect, visited, - allow_dialect_override); + allow_dialect_override, schema); } namespace { @@ -562,8 +684,21 @@ auto sourcemeta::blaze::vocabularies( throw sourcemeta::blaze::SchemaUnknownDialectError(); } - return vocabularies(resolver, resolved_base_dialect.value(), - resolved_dialect); + // A meta-schema that is embedded in the schema itself takes precedence + // over what the resolver knows about, as the schema pins the exact + // meta-schema it is described by + return vocabularies( + [&schema, &resolver](const std::string_view identifier) + -> std::optional { + const auto *embedded{sourcemeta::blaze::metaschema_try_embedded( + schema, identifier, resolver)}; + if (embedded) { + return *embedded; + } + + return resolver(identifier); + }, + resolved_base_dialect.value(), resolved_dialect); } auto sourcemeta::blaze::vocabularies(const SchemaResolver &resolver, diff --git a/vendor/blaze/src/foundation/helpers.h b/vendor/blaze/src/foundation/helpers.h index 8a3d3b19..9345c288 100644 --- a/vendor/blaze/src/foundation/helpers.h +++ b/vendor/blaze/src/foundation/helpers.h @@ -3,8 +3,16 @@ #include -#include // assert -#include // std::string_view +#include + +#include // assert +#include // std::deque +#include // std::initializer_list +#include // std::optional +#include // std::string_view +#include // std::unordered_set +#include // std::pair, std::move +#include // std::vector namespace sourcemeta::blaze { @@ -84,6 +92,126 @@ ref_overrides_adjacent_keywords(const SchemaBaseDialect base_dialect) -> bool { } } +inline auto embedded_metaschema_identifier_matches( + const sourcemeta::core::JSON &candidate, const std::string_view keyword, + const std::string_view identifier, + const std::optional &canonical) -> bool { + const auto *value{ + candidate.try_at(sourcemeta::core::JSON::StringView{keyword})}; + if (!value || !value->is_string()) { + return false; + } + + const auto ¤t{value->to_string()}; + if (current == identifier) { + return true; + } + + if (canonical.has_value()) { + try { + return sourcemeta::core::URI::canonicalize(current) == canonical.value(); + } catch (const sourcemeta::core::URIParseError &) { + return false; + } + } + + return false; +} + +inline auto embedded_metaschema_matches( + const sourcemeta::core::JSON &candidate, const std::string_view identifier, + const std::optional &canonical) -> bool { + if (!candidate.is_object()) { + return false; + } + + for (const auto *const keyword : {"$id", "id"}) { + if (embedded_metaschema_identifier_matches(candidate, keyword, identifier, + canonical)) { + return true; + } + } + + return false; +} + +inline auto +embedded_metaschema_candidate(const sourcemeta::core::JSON &document, + const std::string_view identifier) + -> std::pair { + if (!document.is_object()) { + return {nullptr, ""}; + } + + std::optional canonical; + try { + canonical = sourcemeta::core::URI::canonicalize(identifier); + } catch (const sourcemeta::core::URIParseError &) { + canonical = std::nullopt; + } + + for (const auto *const container : {"$defs", "definitions"}) { + const auto *entries{document.try_at(container)}; + if (!entries || !entries->is_object()) { + continue; + } + + const auto *direct{ + entries->try_at(sourcemeta::core::JSON::StringView{identifier})}; + if (direct && embedded_metaschema_matches(*direct, identifier, canonical)) { + return {direct, container}; + } + + for (const auto &entry : entries->as_object()) { + if (embedded_metaschema_matches(entry.second, identifier, canonical)) { + return {&entry.second, container}; + } + } + } + + return {nullptr, ""}; +} + +inline auto embedded_metaschema_link_valid(const sourcemeta::core::JSON &link, + const std::string_view identifier, + const std::string_view container, + const SchemaBaseDialect base_dialect) + -> bool { + // In 2019-09 and 2020-12, `definitions` is still supported + // for backwards compatibility + switch (base_dialect) { + case SchemaBaseDialect::JSON_Schema_2020_12: + case SchemaBaseDialect::JSON_Schema_2020_12_Hyper: + case SchemaBaseDialect::JSON_Schema_2019_09: + case SchemaBaseDialect::JSON_Schema_2019_09_Hyper: + if (container != "$defs" && container != "definitions") { + return false; + } + + break; + default: + if (container != definitions_keyword(base_dialect)) { + return false; + } + } + + std::optional canonical; + try { + canonical = sourcemeta::core::URI::canonicalize(identifier); + } catch (const sourcemeta::core::URIParseError &) { + canonical = std::nullopt; + } + + return embedded_metaschema_identifier_matches(link, id_keyword(base_dialect), + identifier, canonical); +} + +struct EmbeddedMetaschemaLink { + const sourcemeta::core::JSON *schema; + sourcemeta::core::JSON::StringView identifier; + std::string_view container; +}; + } // namespace sourcemeta::blaze #endif diff --git a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h index 4f8cc5cd..ee394516 100644 --- a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h +++ b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation.h @@ -268,6 +268,43 @@ auto dialect(const sourcemeta::core::JSON &schema, std::string_view default_dialect = "", bool allow_dialect_override = true) -> std::string_view; +/// @ingroup foundation +/// +/// Try to locate the meta-schema that the given schema declares from within +/// the schema itself, as self-contained schemas embed the meta-schemas they +/// depend on. The result points into the given document and is null if no +/// valid embedded meta-schema could be found. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const sourcemeta::core::JSON schema = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://example.com/meta", +/// "$defs": { +/// "https://example.com/meta": { +/// "$id": "https://example.com/meta", +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "object" +/// } +/// } +/// })JSON"); +/// +/// const auto *metaschema{sourcemeta::blaze::metaschema_try_embedded( +/// schema, "https://example.com/meta", +/// sourcemeta::blaze::schema_resolver)}; +/// +/// assert(metaschema); +/// assert(metaschema == &schema.at("$defs").at("https://example.com/meta")); +/// ``` +SOURCEMETA_BLAZE_FOUNDATION_EXPORT +auto metaschema_try_embedded(const sourcemeta::core::JSON &schema, + std::string_view identifier, + const SchemaResolver &resolver) + -> const sourcemeta::core::JSON *; + /// @ingroup foundation /// /// Get the metaschema document that describes the given schema. For example: diff --git a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h index 4ac40dd6..9c6daa99 100644 --- a/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h +++ b/vendor/blaze/src/foundation/include/sourcemeta/blaze/foundation_walker.h @@ -150,7 +150,8 @@ class SOURCEMETA_BLAZE_FOUNDATION_EXPORT SchemaIteratorFlat { /// @ingroup foundation /// /// Return an iterator over the top-level keywords of a given JSON Schema -/// definition in the order in which an implementation must evaluate them. +/// definition in the order in which an implementation must evaluate them, +/// given the vocabularies in use by the schema. /// /// For example: /// @@ -168,10 +169,12 @@ class SOURCEMETA_BLAZE_FOUNDATION_EXPORT SchemaIteratorFlat { /// "patternProperties": {} /// })JSON"); /// +/// const auto vocabularies{sourcemeta::blaze::vocabularies( +/// document, sourcemeta::blaze::schema_resolver)}; +/// /// for (const auto &entry : /// sourcemeta::blaze::SchemaKeywordIterator{ -/// document, sourcemeta::blaze::schema_walker, -/// sourcemeta::blaze::schema_resolver}) { +/// document, sourcemeta::blaze::schema_walker, vocabularies}) { /// sourcemeta::core::stringify(entry.pointer, std::cout); /// std::cout << "\n"; /// } @@ -184,8 +187,7 @@ class SOURCEMETA_BLAZE_FOUNDATION_EXPORT SchemaKeywordIterator { using const_iterator = typename internal::const_iterator; SchemaKeywordIterator(const sourcemeta::core::JSON &input, const SchemaWalker &walker, - const SchemaResolver &resolver, - std::string_view default_dialect = ""); + const Vocabularies &vocabularies); [[nodiscard]] auto begin() const -> const_iterator; [[nodiscard]] auto end() const -> const_iterator; [[nodiscard]] auto cbegin() const -> const_iterator; diff --git a/vendor/blaze/src/foundation/walker.cc b/vendor/blaze/src/foundation/walker.cc index de982ce2..39463db0 100644 --- a/vendor/blaze/src/foundation/walker.cc +++ b/vendor/blaze/src/foundation/walker.cc @@ -419,24 +419,12 @@ sourcemeta::blaze::SchemaIteratorFlat::SchemaIteratorFlat( sourcemeta::blaze::SchemaKeywordIterator::SchemaKeywordIterator( const sourcemeta::core::JSON &schema, const sourcemeta::blaze::SchemaWalker &walker, - const sourcemeta::blaze::SchemaResolver &resolver, - const std::string_view default_dialect) { + const sourcemeta::blaze::Vocabularies &vocabularies) { assert(is_schema(schema)); if (schema.is_boolean()) { return; } - const std::string_view resolved_dialect{ - sourcemeta::blaze::dialect(schema, default_dialect)}; - const auto maybe_base_dialect{ - sourcemeta::blaze::base_dialect(schema, resolver, resolved_dialect)}; - - Vocabularies vocabularies{ - maybe_base_dialect.has_value() && !resolved_dialect.empty() - ? sourcemeta::blaze::vocabularies( - resolver, maybe_base_dialect.value(), resolved_dialect) - : Vocabularies{}}; - // TODO: Use std::ranges::to() once libc++ supports it // (__cpp_lib_ranges_to_container) for (const auto &entry : schema.as_object()) { @@ -445,9 +433,9 @@ sourcemeta::blaze::SchemaKeywordIterator::SchemaKeywordIterator( sourcemeta::blaze::SchemaIteratorEntry subschema_entry{ .parent = std::nullopt, .pointer = std::move(entry_pointer), - .dialect = resolved_dialect, + .dialect = "", .vocabularies = vocabularies, - .base_dialect = maybe_base_dialect, + .base_dialect = std::nullopt, .subschema = entry.second, .orphan = false, .property_name = false}; diff --git a/vendor/blaze/src/frame/frame.cc b/vendor/blaze/src/frame/frame.cc index dd5aa18f..4fca9ba5 100644 --- a/vendor/blaze/src/frame/frame.cc +++ b/vendor/blaze/src/frame/frame.cc @@ -184,11 +184,17 @@ auto find_anchors(const sourcemeta::core::JSON &schema, } } - // Draft 4 + // Draft 4 and 3 // Old `id` anchor form if (schema.is_object() && - vocabularies.contains( - sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_4)) { + (vocabularies.contains( + sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_4) || + vocabularies.contains( + sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_4_Hyper) || + vocabularies.contains( + sourcemeta::blaze::Vocabularies::Known::JSON_Schema_Draft_3) || + vocabularies.contains(sourcemeta::blaze::Vocabularies::Known:: + JSON_Schema_Draft_3_Hyper))) { const auto *id_value{schema.try_at("id")}; if (id_value) { assert(id_value->is_string()); @@ -196,7 +202,7 @@ auto find_anchors(const sourcemeta::core::JSON &schema, // A bare "#" carries no anchor name, so we treat it as no anchor at // all. if (id_view.starts_with('#') && id_view.size() > 1) { - // Draft 4 imposes no plain-name pattern on the fragment, but the + // Draft 4 and 3 impose no plain-name pattern on the fragment, but the // value must still be a valid URI reference per RFC 3986 if (!sourcemeta::core::URI::is_uri_reference(id_view)) { throw sourcemeta::blaze::SchemaKeywordError( @@ -315,6 +321,8 @@ auto supports_id_anchors( case SchemaBaseDialect::JSON_Schema_Draft_6_Hyper: case SchemaBaseDialect::JSON_Schema_Draft_4: case SchemaBaseDialect::JSON_Schema_Draft_4_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_3: + case SchemaBaseDialect::JSON_Schema_Draft_3_Hyper: return true; default: return false; @@ -567,6 +575,28 @@ auto SchemaFrame::analyse(const sourcemeta::core::JSON &root, sourcemeta::core::WeakPointer::Hasher>( paths.cbegin(), paths.cend()) .size() == paths.size())); + + // A meta-schema that is embedded in the document itself takes precedence + // over what the resolver knows about, as the document pins the exact + // meta-schema it is described by + const SchemaResolver effective_resolver{ + [&root, &resolver, this](const std::string_view identifier) + -> std::optional { + const sourcemeta::core::JSON::String key{identifier}; + const auto hit{this->probed_metaschemas_.find(key)}; + if (hit != this->probed_metaschemas_.cend()) { + return *(hit->second); + } + + const auto *match{ + sourcemeta::blaze::metaschema_try_embedded(root, key, resolver)}; + if (match) { + this->probed_metaschemas_.emplace(key, match); + return *match; + } + + return resolver(identifier); + }}; std::vector subschema_entries; std::unordered_map @@ -588,8 +618,8 @@ auto SchemaFrame::analyse(const sourcemeta::core::JSON &root, const auto &schema{sourcemeta::core::get(root, path)}; - const auto root_base_dialect{ - sourcemeta::blaze::base_dialect(schema, resolver, default_dialect)}; + const auto root_base_dialect{sourcemeta::blaze::base_dialect( + schema, effective_resolver, default_dialect)}; if (!root_base_dialect.has_value()) { throw SchemaUnknownBaseDialectError(); } @@ -637,7 +667,7 @@ auto SchemaFrame::analyse(const sourcemeta::core::JSON &root, std::vector current_subschema_entries; for (const auto &relative_entry : sourcemeta::blaze::SchemaIterator{ - schema, walker, resolver, default_dialect}) { + schema, walker, effective_resolver, default_dialect}) { // Rephrase the iterator entry as being for the current base auto entry{relative_entry}; entry.pointer = path.concat(relative_entry.pointer); @@ -1283,8 +1313,25 @@ auto SchemaFrame::root() const noexcept auto SchemaFrame::vocabularies(const Location &location, const SchemaResolver &resolver) const -> Vocabularies { - return sourcemeta::blaze::vocabularies(resolver, location.base_dialect, - location.dialect); + if (this->probed_metaschemas_.empty()) { + return sourcemeta::blaze::vocabularies(resolver, location.base_dialect, + location.dialect); + } + + // Meta-schemas embedded in the analysed document take precedence + // over what the caller's resolver knows about + return sourcemeta::blaze::vocabularies( + [this, &resolver](const std::string_view identifier) + -> std::optional { + const auto hit{this->probed_metaschemas_.find( + sourcemeta::core::JSON::String{identifier})}; + if (hit != this->probed_metaschemas_.cend()) { + return *(hit->second); + } + + return resolver(identifier); + }, + location.base_dialect, location.dialect); } auto SchemaFrame::uri( @@ -1535,6 +1582,7 @@ auto SchemaFrame::reset() -> void { this->root_.clear(); this->locations_.clear(); this->references_.clear(); + this->probed_metaschemas_.clear(); this->standalone_ = false; } diff --git a/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h b/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h index 7481f64d..d78a02e2 100644 --- a/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h +++ b/vendor/blaze/src/frame/include/sourcemeta/blaze/frame.h @@ -298,6 +298,12 @@ class SOURCEMETA_BLAZE_FRAME_EXPORT SchemaFrame { sourcemeta::core::JSON::String root_; Locations locations_; References references_; + // Custom meta-schemas that the resolver could not resolve but that were + // found embedded in the analysed document itself. The values point into + // the analysed document, which the frame must not outlive anyway + std::unordered_map + probed_metaschemas_; mutable std::unordered_map< std::reference_wrapper, std::vector, sourcemeta::core::WeakPointer::Hasher, diff --git a/vendor/core/CMakeLists.txt b/vendor/core/CMakeLists.txt index 6da6d84d..158c5bcf 100644 --- a/vendor/core/CMakeLists.txt +++ b/vendor/core/CMakeLists.txt @@ -31,12 +31,15 @@ option(SOURCEMETA_CORE_YAML "Build the Sourcemeta Core YAML library" ON) option(SOURCEMETA_CORE_JSONRPC "Build the Sourcemeta Core JSON-RPC library" ON) option(SOURCEMETA_CORE_MCP "Build the Sourcemeta Core MCP library" ON) option(SOURCEMETA_CORE_HTTP "Build the Sourcemeta Core HTTP library" ON) +option(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL "Use system cURL for the Sourcemeta Core HTTP library" OFF) +option(SOURCEMETA_CORE_JOSE "Build the Sourcemeta Core JOSE library" ON) option(SOURCEMETA_CORE_SEMVER "Build the Sourcemeta Core SemVer library" ON) option(SOURCEMETA_CORE_GZIP "Build the Sourcemeta Core GZIP library" ON) option(SOURCEMETA_CORE_HTML "Build the Sourcemeta Core HTML library" ON) option(SOURCEMETA_CORE_CSS "Build the Sourcemeta Core CSS library" ON) option(SOURCEMETA_CORE_MARKDOWN "Build the Sourcemeta Core Markdown library" ON) option(SOURCEMETA_CORE_TESTS "Build the Sourcemeta Core tests" OFF) +option(SOURCEMETA_CORE_TESTS_CI "Build the Sourcemeta Core CI tests" OFF) option(SOURCEMETA_CORE_BENCHMARK "Build the Sourcemeta Core benchmarks" OFF) option(SOURCEMETA_CORE_DOCS "Build the Sourcemeta Core docs" OFF) option(SOURCEMETA_CORE_INSTALL "Install the Sourcemeta Core library" ON) @@ -121,7 +124,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 +191,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() @@ -225,7 +232,7 @@ endif() # Testing -if(SOURCEMETA_CORE_CONTRIB_GOOGLETEST OR SOURCEMETA_CORE_TESTS) +if(SOURCEMETA_CORE_CONTRIB_GOOGLETEST OR SOURCEMETA_CORE_TESTS OR SOURCEMETA_CORE_TESTS_CI) find_package(GoogleTest REQUIRED) endif() @@ -346,6 +353,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() @@ -371,6 +382,14 @@ if(SOURCEMETA_CORE_TESTS) endif() endif() +if(SOURCEMETA_CORE_TESTS_CI) + enable_testing() + + if(SOURCEMETA_CORE_HTTP) + add_subdirectory(test/http/ci) + endif() +endif() + if(SOURCEMETA_CORE_BENCHMARK) add_subdirectory(benchmark) endif() diff --git a/vendor/core/DEPENDENCIES b/vendor/core/DEPENDENCIES index 73fe46d9..fb683d43 100644 --- a/vendor/core/DEPENDENCIES +++ b/vendor/core/DEPENDENCIES @@ -3,9 +3,11 @@ jsontestsuite https://github.com/nst/JSONTestSuite d64aefb55228d9584d3e5b2433f72 yaml-test-suite https://github.com/yaml/yaml-test-suite data-2022-01-17 cmark-gfm https://github.com/github/cmark-gfm 587a12bb54d95ac37241377e6ddc93ea0e45439b uritemplate-test https://github.com/uri-templates/uritemplate-test 1eb27ab4462b9e5819dc47db99044f5fd1fa9bc7 -pyca-cryptography https://github.com/pyca/cryptography c4935a7021af37c38e0684b0546c1b4378518342 +pyca-cryptography https://github.com/pyca/cryptography 9747d06e83764e7f1ea4c04daf134cb8f861700b +wycheproof https://github.com/C2SP/wycheproof 6d7cccd0fcb1917368579adeeac10fe802f1b521 pcre2 https://github.com/PCRE2Project/pcre2 pcre2-10.47 googletest https://github.com/google/googletest a7f443b80b105f940225332ed3c31f2790092f47 googlebenchmark https://github.com/google/benchmark 378fe693a1ef51500db21b11ff05a8018c5f0e55 libdeflate https://github.com/ebiggers/libdeflate v1.25 unicodetools https://github.com/unicode-org/unicodetools final-17.0-20250910 +jose-cookbook https://github.com/ietf-jose/cookbook 13692b68bfc18b99557a5b1ed311fd5077bfff04 diff --git a/vendor/core/cmake/common/compiler/options.cmake b/vendor/core/cmake/common/compiler/options.cmake index 15079925..20c05eaa 100644 --- a/vendor/core/cmake/common/compiler/options.cmake +++ b/vendor/core/cmake/common/compiler/options.cmake @@ -116,6 +116,10 @@ function(sourcemeta_add_default_options visibility target) -Wno-exit-time-destructors -Wrange-loop-analysis + # Manage Objective-C and Objective-C++ object lifetimes with Automatic + # Reference Counting + $<$,$>:-fobjc-arc> + # Enable loop vectorization for performance reasons $<$>:-fvectorize> # Enable vectorization of straight-line code for performance diff --git a/vendor/core/cmake/common/variables.cmake b/vendor/core/cmake/common/variables.cmake index ee6359cb..c7d6adbb 100644 --- a/vendor/core/cmake/common/variables.cmake +++ b/vendor/core/cmake/common/variables.cmake @@ -1,3 +1,10 @@ +# Objective-C++ powers the Apple-specific backends and must be enabled before +# we capture the project languages below, so its standard and visibility +# defaults get applied +if(APPLE) + enable_language(OBJCXX) +endif() + # Get the list of languages defined in the project get_property(SOURCEMETA_LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES) diff --git a/vendor/core/config.cmake.in b/vendor/core/config.cmake.in index e6a48152..16d87fed 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") @@ -153,18 +158,38 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonrpc.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_mcp.cmake") elseif(component STREQUAL "http") + if(@SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL@) + find_dependency(CURL) + endif() include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") 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_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_time.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_crypto.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jose.cmake") 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..679bfd55 100644 --- a/vendor/core/src/core/crypto/CMakeLists.txt +++ b/vendor/core/src/core/crypto/CMakeLists.txt @@ -1,12 +1,138 @@ 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_openssl.cc) target_link_libraries(sourcemeta_core_crypto PRIVATE OpenSSL::Crypto) +elseif(APPLE) + enable_language(OBJCXX) + + # Ed25519 is verified through CryptoKit (Swift), reached from Objective-C++. + # The Swift shim is compiled to an object and its generated Objective-C + # interface header out of band, since the rest of the library is C++ + set(CRYPTOKIT_SWIFT "${CMAKE_CURRENT_SOURCE_DIR}/crypto_eddsa_cryptokit.swift") + set(CRYPTOKIT_HEADER + "${CMAKE_CURRENT_BINARY_DIR}/sourcemeta_core_cryptokit-Swift.h") + if(CMAKE_OSX_SYSROOT) + set(CRYPTOKIT_SDK "${CMAKE_OSX_SYSROOT}") + else() + execute_process(COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE CRYPTOKIT_SDK OUTPUT_STRIP_TRAILING_WHITESPACE) + endif() + + # The toolchain directory holding the Swift runtime back-deployment archives + # that the shim autolinks for older deployment targets + execute_process(COMMAND xcrun --find swiftc + OUTPUT_VARIABLE CRYPTOKIT_SWIFTC OUTPUT_STRIP_TRAILING_WHITESPACE) + get_filename_component(CRYPTOKIT_TOOLCHAIN_BIN "${CRYPTOKIT_SWIFTC}" DIRECTORY) + get_filename_component(CRYPTOKIT_RUNTIME_LIB + "${CRYPTOKIT_TOOLCHAIN_BIN}/../lib/swift/macosx" ABSOLUTE) + if(CMAKE_OSX_DEPLOYMENT_TARGET) + set(CRYPTOKIT_DEPLOYMENT "${CMAKE_OSX_DEPLOYMENT_TARGET}") + else() + # CryptoKit signing over Curve25519 is available since macOS 10.15 + set(CRYPTOKIT_DEPLOYMENT "10.15") + endif() + if(CMAKE_OSX_ARCHITECTURES) + set(CRYPTOKIT_ARCHITECTURES ${CMAKE_OSX_ARCHITECTURES}) + else() + set(CRYPTOKIT_ARCHITECTURES "${CMAKE_SYSTEM_PROCESSOR}") + endif() + + # One object per architecture, emitting the architecture independent header + # only on the first, combined afterwards into a single object + set(CRYPTOKIT_OBJECTS) + set(CRYPTOKIT_HEADER_OUTPUT "${CRYPTOKIT_HEADER}") + set(CRYPTOKIT_EMIT_HEADER + -emit-objc-header -emit-objc-header-path "${CRYPTOKIT_HEADER}") + foreach(architecture IN LISTS CRYPTOKIT_ARCHITECTURES) + set(CRYPTOKIT_ARCH_OBJECT + "${CMAKE_CURRENT_BINARY_DIR}/crypto_eddsa_cryptokit_${architecture}.o") + add_custom_command( + OUTPUT "${CRYPTOKIT_ARCH_OBJECT}" ${CRYPTOKIT_HEADER_OUTPUT} + COMMAND xcrun swiftc + -sdk "${CRYPTOKIT_SDK}" + -target "${architecture}-apple-macosx${CRYPTOKIT_DEPLOYMENT}" + -module-name sourcemeta_core_cryptokit + -parse-as-library -O + ${CRYPTOKIT_EMIT_HEADER} + -emit-object -o "${CRYPTOKIT_ARCH_OBJECT}" + "${CRYPTOKIT_SWIFT}" + DEPENDS "${CRYPTOKIT_SWIFT}" + COMMENT "Building CryptoKit Swift shim (${architecture})" + VERBATIM) + list(APPEND CRYPTOKIT_OBJECTS "${CRYPTOKIT_ARCH_OBJECT}") + set(CRYPTOKIT_HEADER_OUTPUT) + set(CRYPTOKIT_EMIT_HEADER) + endforeach() + + list(LENGTH CRYPTOKIT_OBJECTS CRYPTOKIT_OBJECT_COUNT) + if(CRYPTOKIT_OBJECT_COUNT GREATER 1) + set(CRYPTOKIT_OBJECT "${CMAKE_CURRENT_BINARY_DIR}/crypto_eddsa_cryptokit.o") + add_custom_command( + OUTPUT "${CRYPTOKIT_OBJECT}" + COMMAND lipo -create ${CRYPTOKIT_OBJECTS} -output "${CRYPTOKIT_OBJECT}" + DEPENDS ${CRYPTOKIT_OBJECTS} + COMMENT "Combining CryptoKit Swift shim architectures" + VERBATIM) + else() + set(CRYPTOKIT_OBJECT "${CRYPTOKIT_OBJECTS}") + endif() + + set_source_files_properties("${CRYPTOKIT_OBJECT}" + PROPERTIES EXTERNAL_OBJECT TRUE GENERATED TRUE) + set_source_files_properties("${CRYPTOKIT_HEADER}" PROPERTIES GENERATED TRUE) + set_source_files_properties(crypto_eddsa_cryptokit.mm + PROPERTIES OBJECT_DEPENDS "${CRYPTOKIT_HEADER}") + + target_sources(sourcemeta_core_crypto PRIVATE + crypto_sha1_apple.cc crypto_sha256_apple.cc crypto_sha384_apple.cc + crypto_sha512_apple.cc crypto_random_apple.cc + crypto_verify_apple.cc + crypto_eddsa_cryptokit.mm "${CRYPTOKIT_OBJECT}" + crypto_eddsa.h crypto_eddsa_apple.h crypto_bignum.h crypto_shake256.h) + + # The generated Objective-C interface header lives in the build tree + target_include_directories(sourcemeta_core_crypto + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") + + target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework Security") + target_link_libraries(sourcemeta_core_crypto + PRIVATE "-framework CoreFoundation") + target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework CryptoKit") + target_link_libraries(sourcemeta_core_crypto PRIVATE "-framework Foundation") + + # Resolve the Swift runtime that the shim autolinks, both at link and at load. + # PUBLIC rather than INTERFACE so that a shared build of this library, which + # has its own link step pulling in the Swift object, also gets the flags + target_link_options(sourcemeta_core_crypto PUBLIC + "SHELL:-L ${CRYPTOKIT_SDK}/usr/lib/swift" + "SHELL:-L ${CRYPTOKIT_RUNTIME_LIB}" + "SHELL:-Xlinker -rpath -Xlinker /usr/lib/swift") +elseif(WIN32) + target_sources(sourcemeta_core_crypto PRIVATE + crypto_sha1_windows.cc crypto_sha256_windows.cc crypto_sha384_windows.cc + crypto_sha512_windows.cc crypto_random_windows.cc + crypto_verify_windows.cc crypto_eddsa.h + crypto_bignum.h crypto_shake256.h) + target_link_libraries(sourcemeta_core_crypto PRIVATE bcrypt) +else() + message(WARNING "Building the reference cryptography backend, instead of the " + "OpenSSL recommended production one") + target_sources(sourcemeta_core_crypto PRIVATE + crypto_sha1_other.cc crypto_sha256_other.cc crypto_sha384_other.cc + crypto_sha512_other.cc crypto_random_other.cc crypto_verify_other.cc + crypto_bignum.h crypto_ecc.h crypto_eddsa.h crypto_shake256.h) endif() if(SOURCEMETA_CORE_INSTALL) diff --git a/vendor/core/src/core/crypto/crypto_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..5244bc25 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_bignum.h @@ -0,0 +1,507 @@ +#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. Constant-time execution is not +// required, since 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) -> 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 { + const auto word{bit / 64}; + if (word >= value.size) { + return false; + } + + return ((value.words[word] >> (bit % 64)) & 1u) != 0; +} + +// Assumes the result fits in the capacity +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_shift_right(const Bignum &value, + const std::size_t bits) noexcept -> Bignum; + +// Reduce a value modulo the modulus with Knuth's Algorithm D (TAOCP Volume 2, +// Section 4.3.1), the schoolbook long division that estimates one quotient +// word per step rather than one bit, so the cost is quadratic in the number of +// words rather than the number of bits +inline auto bignum_reduce(Bignum &value, const Bignum &modulus) noexcept + -> void { + if (bignum_compare(value, modulus) < 0) { + return; + } + + const auto divisor_words{modulus.size}; + + // A single-word divisor folds the value down word by word + if (divisor_words == 1) { + const auto divisor{modulus.words[0]}; + BignumDoubleWord remainder{0}; + for (std::size_t index = value.size; index > 0; --index) { + remainder = (remainder << 64u) | value.words[index - 1]; + remainder %= divisor; + } + + value = bignum_from_u64(static_cast(remainder)); + return; + } + + // Normalize so the divisor's top word has its high bit set, which bounds the + // error of each quotient word estimate to at most two + const auto shift{static_cast( + (64u - (bignum_bit_length(modulus) % 64u)) % 64u)}; + const auto divisor{shift > 0 ? bignum_shift_left(modulus, shift) : modulus}; + auto dividend{shift > 0 ? bignum_shift_left(value, shift) : value}; + const auto dividend_words{dividend.size}; + const auto quotient_words{dividend_words - divisor_words}; + const auto top{divisor.words[divisor_words - 1]}; + const auto next{divisor.words[divisor_words - 2]}; + const BignumDoubleWord base{static_cast(1) << 64u}; + + for (std::size_t step = quotient_words + 1; step > 0; --step) { + const auto offset{step - 1}; + + // Estimate the quotient word from the top two words of the running value + const auto numerator{ + (static_cast(dividend.words[offset + divisor_words]) + << 64u) | + dividend.words[offset + divisor_words - 1]}; + auto estimate{numerator / top}; + auto estimate_remainder{numerator % top}; + while (estimate >= base || + estimate * next > (estimate_remainder << 64u) + + dividend.words[offset + divisor_words - 2]) { + estimate -= 1; + estimate_remainder += top; + if (estimate_remainder >= base) { + break; + } + } + + // Multiply the divisor by the estimate and subtract from the running value + const auto quotient_word{static_cast(estimate)}; + BignumDoubleWord carry{0}; + std::uint64_t borrow{0}; + for (std::size_t index = 0; index < divisor_words; ++index) { + const auto product{static_cast(quotient_word) * + divisor.words[index] + + carry}; + carry = product >> 64u; + const auto subtrahend{static_cast(product)}; + const auto current{dividend.words[offset + index]}; + const auto without_subtrahend{current - subtrahend}; + auto next_borrow{current < subtrahend ? 1u : 0u}; + const auto result_word{without_subtrahend - borrow}; + if (without_subtrahend < borrow) { + next_borrow += 1u; + } + + dividend.words[offset + index] = result_word; + borrow = next_borrow; + } + + const auto current{dividend.words[offset + divisor_words]}; + const auto subtrahend{static_cast(carry)}; + const auto without_subtrahend{current - subtrahend}; + auto next_borrow{current < subtrahend ? 1u : 0u}; + dividend.words[offset + divisor_words] = without_subtrahend - borrow; + if (without_subtrahend < borrow) { + next_borrow += 1u; + } + + // The estimate was at most one too large, so add the divisor back when the + // subtraction borrowed past the top + if (next_borrow != 0) { + BignumDoubleWord add_carry{0}; + for (std::size_t index = 0; index < divisor_words; ++index) { + const auto sum{ + static_cast(dividend.words[offset + index]) + + divisor.words[index] + add_carry}; + dividend.words[offset + index] = static_cast(sum); + add_carry = sum >> 64u; + } + + dividend.words[offset + divisor_words] += + static_cast(add_carry); + } + } + + // The remainder occupies the low words, still scaled by the normalization + dividend.size = divisor_words; + bignum_normalize(dividend); + value = shift > 0 ? bignum_shift_right(dividend, shift) : dividend; +} + +// Assumes both operands fit in half the capacity +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; +} + +// Halve a value modulo an odd modulus: an even value shifts down, an odd one +// becomes even by adding the modulus first, so the result stays an integer +inline auto bignum_mod_halve(const Bignum &value, + const Bignum &modulus) noexcept -> Bignum { + if ((value.words[0] & 1u) == 0) { + return bignum_shift_right(value, 1); + } + + return bignum_shift_right(bignum_add(value, modulus), 1); +} + +// Modular inverse by the binary extended Euclidean algorithm, which needs only +// halving, subtraction, and comparison rather than the modular exponentiation +// a Fermat inverse over a prime modulus would spend. The modulus must be odd. +// Returns zero when the value has no inverse, which is when it shares a factor +// with the modulus or reduces to zero +inline auto bignum_mod_inverse(const Bignum &value, + const Bignum &modulus) noexcept -> Bignum { + const auto one{bignum_from_u64(1)}; + auto first{value}; + bignum_reduce(first, modulus); + auto second{modulus}; + auto first_coefficient{one}; + Bignum second_coefficient; + + while (bignum_compare(first, one) != 0 && bignum_compare(second, one) != 0) { + // A side reaching zero means the greatest common divisor exceeds one, so no + // inverse exists. Stopping here also keeps the halving below from spinning + // forever on a zero value + if (bignum_is_zero(first) || bignum_is_zero(second)) { + return {}; + } + + while ((first.words[0] & 1u) == 0) { + first = bignum_shift_right(first, 1); + first_coefficient = bignum_mod_halve(first_coefficient, modulus); + } + + while ((second.words[0] & 1u) == 0) { + second = bignum_shift_right(second, 1); + second_coefficient = bignum_mod_halve(second_coefficient, modulus); + } + + if (bignum_compare(first, second) >= 0) { + bignum_subtract_in_place(first, second); + first_coefficient = + bignum_mod_subtract(first_coefficient, second_coefficient, modulus); + } else { + bignum_subtract_in_place(second, first); + second_coefficient = + bignum_mod_subtract(second_coefficient, first_coefficient, modulus); + } + } + + return bignum_compare(first, one) == 0 ? first_coefficient + : second_coefficient; +} + +inline auto bignum_to_bytes(const Bignum &value, const std::size_t length) + -> 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..2a4de5e0 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_ecc.h @@ -0,0 +1,522 @@ +#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. Constant time execution is +// not required, since verification consumes only public inputs + +#include "crypto_bignum.h" + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint64_t +#include // std::string_view + +namespace sourcemeta::core { + +// Identifies the field prime so that modular reduction can take the fast +// generalized Mersenne path specific to each NIST curve +enum class NISTPrime : std::uint8_t { P256, P384, P521 }; + +struct EllipticCurveParameters { + Bignum prime; + Bignum coefficient_a; + Bignum coefficient_b; + Bignum generator_x; + Bignum generator_y; + Bignum order; + std::size_t field_bytes; + NISTPrime reduction; +}; + +// 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 {.prime = + bignum_from_hex("ffffffff00000001000000000000000000000000ffffffff" + "ffffffffffffffff"), + .coefficient_a = + bignum_from_hex("ffffffff00000001000000000000000000000000ffffffff" + "fffffffffffffffc"), + .coefficient_b = + bignum_from_hex("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f6" + "3bce3c3e27d2604b"), + .generator_x = + bignum_from_hex("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0" + "f4a13945d898c296"), + .generator_y = + bignum_from_hex("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ece" + "cbb6406837bf51f5"), + .order = + bignum_from_hex("ffffffff00000000ffffffffffffffffbce6faada7179e84" + "f3b9cac2fc632551"), + .field_bytes = 32, + .reduction = NISTPrime::P256}; +} + +inline auto curve_p384() -> EllipticCurveParameters { + // The hexadecimal constants are single string literals so that no digit + // is ever lost across a line break + // clang-format off + return { + .prime = bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff"), + .coefficient_a = bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc"), + .coefficient_b = bignum_from_hex("b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef"), + .generator_x = bignum_from_hex("aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7"), + .generator_y = bignum_from_hex("3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f"), + .order = bignum_from_hex("ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973"), + .field_bytes = 48, + .reduction = NISTPrime::P384}; + // clang-format on +} + +inline auto curve_p521() -> EllipticCurveParameters { + // clang-format off + return { + .prime = bignum_from_hex("01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + .coefficient_a = bignum_from_hex("01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc"), + .coefficient_b = bignum_from_hex("0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00"), + .generator_x = bignum_from_hex("00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66"), + .generator_y = bignum_from_hex("011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650"), + .order = bignum_from_hex("01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409"), + .field_bytes = 66, + .reduction = NISTPrime::P521}; + // clang-format on +} + +// NIST P-521 field reduction. The prime is 2^521 - 1, so 2^521 is congruent +// to 1 modulo it, and a value below the square of the prime folds the bits +// above position 521 back into the low 521 bits with a single addition +inline auto field_reduce_p521(Bignum &value, const Bignum &prime) noexcept + -> void { + auto high{bignum_shift_right(value, 521)}; + if (value.size > 8) { + value.words[8] &= 0x1ffULL; + for (std::size_t index = 9; index < value.size; ++index) { + value.words[index] = 0; + } + + value.size = 9; + bignum_normalize(value); + } + + value = bignum_add(value, high); + while (bignum_compare(value, prime) >= 0) { + bignum_subtract_in_place(value, prime); + } +} + +// Read the 32-bit limb at the given position, counting from the least +// significant, returning zero past the end of the value +inline auto field_word(const Bignum &value, const std::size_t index) noexcept + -> std::uint64_t { + const auto word{index / 2}; + if (word >= value.size) { + return 0; + } + + return (value.words[word] >> (32 * (index % 2))) & 0xffffffffULL; +} + +// Add a reduction term, given as its 32-bit limbs most significant first, +// into the column accumulator scaled by the multiplier. Column zero is the +// least significant, and accumulating into 64-bit columns rather than +// materializing a value per term keeps the reduction free of temporaries +template +inline auto field_accumulate(std::array &columns, + const std::array &limbs, + const std::uint64_t multiplier) noexcept -> void { + for (std::size_t index = 0; index < Count; ++index) { + columns[Count - 1 - index] += multiplier * limbs[index]; + } +} + +// Carry propagate the 32-bit columns and pack them into a value +template +inline auto field_from_columns(std::array &columns) + -> Bignum { + std::uint64_t carry{0}; + Bignum result; + for (std::size_t index = 0; index <= Count; ++index) { + const auto current{columns[index] + carry}; + result.words[index / 2] |= (current & 0xffffffffULL) << (32 * (index % 2)); + carry = current >> 32; + } + + result.size = (Count + 2) / 2; + bignum_normalize(result); + return result; +} + +// Combine the positive and negative column sums of a generalized Mersenne +// reduction into the single reduced value below the prime +inline auto field_combine(Bignum &positive, const Bignum &negative, + const Bignum &prime) noexcept -> void { + while (bignum_compare(positive, negative) < 0) { + positive = bignum_add(positive, prime); + } + + bignum_subtract_in_place(positive, negative); + while (bignum_compare(positive, prime) >= 0) { + bignum_subtract_in_place(positive, prime); + } +} + +// NIST P-256 field reduction. The prime is 2^256 - 2^224 + 2^192 + 2^96 - 1, +// a generalized Mersenne prime whose reduction recombines the 32-bit limbs of +// the product into a small signed sum of nine field-width terms +// (FIPS 186-4 Appendix D.2.3) +inline auto field_reduce_p256(Bignum &value, const Bignum &prime) noexcept + -> void { + std::array c{}; + for (std::size_t index = 0; index < 16; ++index) { + c[index] = field_word(value, index); + } + + std::array positive_columns{}; + std::array negative_columns{}; + field_accumulate<8>(positive_columns, + {{c[7], c[6], c[5], c[4], c[3], c[2], c[1], c[0]}}, 1); + field_accumulate<8>(positive_columns, + {{c[15], c[14], c[13], c[12], c[11], 0, 0, 0}}, 2); + field_accumulate<8>(positive_columns, + {{0, c[15], c[14], c[13], c[12], 0, 0, 0}}, 2); + field_accumulate<8>(positive_columns, + {{c[15], c[14], 0, 0, 0, c[10], c[9], c[8]}}, 1); + field_accumulate<8>(positive_columns, + {{c[8], c[13], c[15], c[14], c[13], c[11], c[10], c[9]}}, + 1); + field_accumulate<8>(negative_columns, + {{c[10], c[8], 0, 0, 0, c[13], c[12], c[11]}}, 1); + field_accumulate<8>(negative_columns, + {{c[11], c[9], 0, 0, c[15], c[14], c[13], c[12]}}, 1); + field_accumulate<8>(negative_columns, + {{c[12], 0, c[10], c[9], c[8], c[15], c[14], c[13]}}, 1); + field_accumulate<8>(negative_columns, + {{c[13], 0, c[11], c[10], c[9], 0, c[15], c[14]}}, 1); + + auto positive{field_from_columns<8>(positive_columns)}; + const auto negative{field_from_columns<8>(negative_columns)}; + field_combine(positive, negative, prime); + value = positive; +} + +// NIST P-384 field reduction. The prime is 2^384 - 2^128 - 2^96 + 2^32 - 1, +// a generalized Mersenne prime whose reduction recombines the 32-bit limbs of +// the product into a small signed sum of ten field-width terms +// (FIPS 186-4 Appendix D.2.4) +inline auto field_reduce_p384(Bignum &value, const Bignum &prime) noexcept + -> void { + std::array c{}; + for (std::size_t index = 0; index < 24; ++index) { + c[index] = field_word(value, index); + } + + std::array positive_columns{}; + std::array negative_columns{}; + field_accumulate<12>(positive_columns, + {{c[11], c[10], c[9], c[8], c[7], c[6], c[5], c[4], c[3], + c[2], c[1], c[0]}}, + 1); + field_accumulate<12>(positive_columns, + {{0, 0, 0, 0, 0, c[23], c[22], c[21], 0, 0, 0, 0}}, 2); + field_accumulate<12>(positive_columns, + {{c[23], c[22], c[21], c[20], c[19], c[18], c[17], c[16], + c[15], c[14], c[13], c[12]}}, + 1); + field_accumulate<12>(positive_columns, + {{c[20], c[19], c[18], c[17], c[16], c[15], c[14], c[13], + c[12], c[23], c[22], c[21]}}, + 1); + field_accumulate<12>(positive_columns, + {{c[19], c[18], c[17], c[16], c[15], c[14], c[13], c[12], + c[20], 0, c[23], 0}}, + 1); + field_accumulate<12>(positive_columns, + {{0, 0, 0, 0, c[23], c[22], c[21], c[20], 0, 0, 0, 0}}, + 1); + field_accumulate<12>(positive_columns, + {{0, 0, 0, 0, 0, 0, c[23], c[22], c[21], 0, 0, c[20]}}, + 1); + field_accumulate<12>(negative_columns, + {{c[22], c[21], c[20], c[19], c[18], c[17], c[16], c[15], + c[14], c[13], c[12], c[23]}}, + 1); + field_accumulate<12>(negative_columns, + {{0, 0, 0, 0, 0, 0, 0, c[23], c[22], c[21], c[20], 0}}, + 1); + field_accumulate<12>(negative_columns, + {{0, 0, 0, 0, 0, 0, 0, c[23], c[23], 0, 0, 0}}, 1); + + auto positive{field_from_columns<12>(positive_columns)}; + const auto negative{field_from_columns<12>(negative_columns)}; + field_combine(positive, negative, prime); + value = positive; +} + +// Reduce a product below the square of the field prime, taking the fast +// generalized Mersenne path for the curve rather than long division +inline auto field_reduce(Bignum &value, + const EllipticCurveParameters &curve) noexcept + -> void { + switch (curve.reduction) { + case NISTPrime::P521: + field_reduce_p521(value, curve.prime); + return; + case NISTPrime::P256: + field_reduce_p256(value, curve.prime); + return; + case NISTPrime::P384: + field_reduce_p384(value, curve.prime); + return; + } +} + +inline auto field_mod_multiply(const Bignum &left, const Bignum &right, + const EllipticCurveParameters &curve) noexcept + -> Bignum { + auto result{bignum_multiply(left, right)}; + field_reduce(result, curve); + return result; +} + +inline auto field_square(const Bignum &value, + const EllipticCurveParameters &curve) noexcept + -> Bignum { + return field_mod_multiply(value, value, curve); +} + +inline auto point_is_infinity(const JacobianPoint &point) noexcept -> bool { + return bignum_is_zero(point.z); +} + +// 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 {}; + } + + // The doubling formula for curves with coefficient -3 (EFD dbl-2001-b), + // which trades the coefficient multiplication and a squaring for one more + // subtraction, and computes every small multiple as a chain of modular + // additions so that no division-based reduction is spent on a constant + const auto &prime{curve.prime}; + const auto delta{field_square(point.z, curve)}; + const auto gamma{field_square(point.y, curve)}; + const auto beta{field_mod_multiply(point.x, gamma, curve)}; + const auto difference{ + field_mod_multiply(bignum_mod_subtract(point.x, delta, prime), + bignum_mod_add(point.x, delta, prime), curve)}; + const auto alpha{bignum_mod_add(bignum_mod_add(difference, difference, prime), + difference, prime)}; + const auto two_beta{bignum_mod_add(beta, beta, prime)}; + const auto four_beta{bignum_mod_add(two_beta, two_beta, prime)}; + const auto eight_beta{bignum_mod_add(four_beta, four_beta, prime)}; + const auto result_x{ + bignum_mod_subtract(field_square(alpha, curve), eight_beta, prime)}; + const auto y_plus_z{bignum_mod_add(point.y, point.z, prime)}; + const auto result_z{bignum_mod_subtract( + bignum_mod_subtract(field_square(y_plus_z, curve), gamma, prime), delta, + prime)}; + const auto gamma_squared{field_square(gamma, curve)}; + const auto two_gamma_squared{ + bignum_mod_add(gamma_squared, gamma_squared, prime)}; + const auto four_gamma_squared{ + bignum_mod_add(two_gamma_squared, two_gamma_squared, prime)}; + const auto eight_gamma_squared{ + bignum_mod_add(four_gamma_squared, four_gamma_squared, prime)}; + const auto result_y{bignum_mod_subtract( + field_mod_multiply(alpha, bignum_mod_subtract(four_beta, result_x, prime), + curve), + eight_gamma_squared, prime)}; + return {.x = result_x, .y = result_y, .z = result_z}; +} + +// Point addition in Jacobian coordinates +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{field_mod_multiply(left.z, left.z, curve)}; + const auto right_z_squared{field_mod_multiply(right.z, right.z, curve)}; + const auto u1{field_mod_multiply(left.x, right_z_squared, curve)}; + const auto u2{field_mod_multiply(right.x, left_z_squared, curve)}; + const auto left_z_cubed{field_mod_multiply(left_z_squared, left.z, curve)}; + const auto right_z_cubed{field_mod_multiply(right_z_squared, right.z, curve)}; + const auto s1{field_mod_multiply(left.y, right_z_cubed, curve)}; + const auto s2{field_mod_multiply(right.y, left_z_cubed, curve)}; + + if (bignum_compare(u1, u2) == 0) { + if (bignum_compare(s1, s2) != 0) { + return {}; + } + + 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{field_square(h, curve)}; + const auto h_cubed{field_mod_multiply(h_squared, h, curve)}; + const auto u1_h_squared{field_mod_multiply(u1, h_squared, curve)}; + const auto result_x{bignum_mod_subtract( + bignum_mod_subtract(field_square(r, curve), h_cubed, prime), + field_mod_multiply(bignum_from_u64(2), u1_h_squared, curve), prime)}; + const auto result_y{bignum_mod_subtract( + field_mod_multiply(r, bignum_mod_subtract(u1_h_squared, result_x, prime), + curve), + field_mod_multiply(s1, h_cubed, curve), prime)}; + const auto result_z{ + field_mod_multiply(field_mod_multiply(h, left.z, curve), right.z, curve)}; + return {.x = result_x, .y = result_y, .z = result_z}; +} + +// Add a Jacobian point and an affine point whose Z coordinate is one, the case +// that arises when accumulating the fixed input points in the combined ladder. +// Skipping the second point's Z powers saves several multiplications over the +// general addition (EFD madd-2007-bl) +inline auto point_add_mixed(const JacobianPoint &left, + const JacobianPoint &right, + const EllipticCurveParameters &curve) + -> JacobianPoint { + if (point_is_infinity(left)) { + return right; + } + + if (point_is_infinity(right)) { + return left; + } + + const auto &prime{curve.prime}; + const auto z_squared{field_square(left.z, curve)}; + const auto u2{field_mod_multiply(right.x, z_squared, curve)}; + const auto s2{field_mod_multiply( + right.y, field_mod_multiply(left.z, z_squared, curve), curve)}; + const auto h{bignum_mod_subtract(u2, left.x, prime)}; + + if (bignum_is_zero(h)) { + if (bignum_compare(s2, left.y) == 0) { + return point_double(left, curve); + } + + return {}; + } + + const auto h_squared{field_square(h, curve)}; + const auto two_h_squared{bignum_mod_add(h_squared, h_squared, prime)}; + const auto scaled_h_squared{ + bignum_mod_add(two_h_squared, two_h_squared, prime)}; + const auto j{field_mod_multiply(h, scaled_h_squared, curve)}; + const auto s_difference{bignum_mod_subtract(s2, left.y, prime)}; + const auto r{bignum_mod_add(s_difference, s_difference, prime)}; + const auto v{field_mod_multiply(left.x, scaled_h_squared, curve)}; + const auto two_v{bignum_mod_add(v, v, prime)}; + const auto result_x{bignum_mod_subtract( + bignum_mod_subtract(field_mod_multiply(r, r, curve), j, prime), two_v, + prime)}; + const auto y_j{field_mod_multiply(left.y, j, curve)}; + const auto two_y_j{bignum_mod_add(y_j, y_j, prime)}; + const auto result_y{bignum_mod_subtract( + field_mod_multiply(r, bignum_mod_subtract(v, result_x, prime), curve), + two_y_j, prime)}; + const auto z_plus_h{bignum_mod_add(left.z, h, prime)}; + const auto result_z{bignum_mod_subtract( + bignum_mod_subtract(field_square(z_plus_h, curve), z_squared, prime), + h_squared, prime)}; + return {.x = result_x, .y = result_y, .z = result_z}; +} + +// Normalize a Jacobian point to the affine representation with Z coordinate +// one, so that later additions can take the cheaper mixed path +inline auto point_to_affine(const JacobianPoint &point, + const EllipticCurveParameters &curve) + -> JacobianPoint { + if (point_is_infinity(point)) { + return {}; + } + + const auto z_inverse{bignum_mod_inverse(point.z, curve.prime)}; + const auto z_inverse_squared{field_square(z_inverse, curve)}; + const auto z_inverse_cubed{ + field_mod_multiply(z_inverse_squared, z_inverse, curve)}; + return {.x = field_mod_multiply(point.x, z_inverse_squared, curve), + .y = field_mod_multiply(point.y, z_inverse_cubed, curve), + .z = bignum_from_u64(1)}; +} + +// Compute scalar_one * point_one + scalar_two * point_two with Shamir's trick, +// a single double-and-add over the longer scalar that adds a precomputed sum +// whenever both scalars have a set bit, halving the doublings of two separate +// scalar multiplications. The three addable points are kept affine so every +// step takes the mixed addition +inline auto point_double_scalar_multiply(const Bignum &scalar_one, + const JacobianPoint &point_one, + const Bignum &scalar_two, + const JacobianPoint &point_two, + const EllipticCurveParameters &curve) + -> JacobianPoint { + const auto combined{ + point_to_affine(point_add(point_one, point_two, curve), curve)}; + JacobianPoint result{}; + const auto bits_one{bignum_bit_length(scalar_one)}; + const auto bits_two{bignum_bit_length(scalar_two)}; + const auto bits{bits_one > bits_two ? bits_one : bits_two}; + for (std::size_t index = bits; index > 0; --index) { + result = point_double(result, curve); + const auto bit_one{bignum_get_bit(scalar_one, index - 1)}; + const auto bit_two{bignum_get_bit(scalar_two, index - 1)}; + if (bit_one && bit_two) { + result = point_add_mixed(result, combined, curve); + } else if (bit_one) { + result = point_add_mixed(result, point_one, curve); + } else if (bit_two) { + result = point_add_mixed(result, point_two, curve); + } + } + + 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{field_square(z_inverse, curve)}; + return field_mod_multiply(point.x, z_inverse_squared, curve); +} + +// Whether the affine point satisfies y^2 = x^3 + a*x + b (mod p) +inline auto point_on_curve(const Bignum &x, const Bignum &y, + const EllipticCurveParameters &curve) -> bool { + const auto &prime{curve.prime}; + const auto left{field_square(y, curve)}; + const auto x_cubed{field_mod_multiply(field_square(x, curve), x, curve)}; + const auto right{bignum_mod_add( + bignum_mod_add(x_cubed, field_mod_multiply(curve.coefficient_a, x, curve), + prime), + curve.coefficient_b, prime)}; + return bignum_compare(left, right) == 0; +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_eddsa.h b/vendor/core/src/core/crypto/crypto_eddsa.h new file mode 100644 index 00000000..1ff0da48 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa.h @@ -0,0 +1,448 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_EDDSA_H_ +#define SOURCEMETA_CORE_CRYPTO_EDDSA_H_ + +// Edwards-curve signature verification (Ed25519 and Ed448, RFC 8032 Section 5, +// the pure variants) for the backends without a native EdDSA primitive. Points +// are kept in extended Edwards coordinates, so that the group law is a single +// set of complete formulas shared by both curves. Constant time execution is +// not required, since verification consumes only public inputs + +#include + +#include "crypto_bignum.h" +#include "crypto_shake256.h" + +#include // std::size_t +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +// A point in extended Edwards coordinates (X : Y : Z : T), where the affine +// point is (X / Z, Y / Z) and T = X * Y / Z (RFC 8032 Section 5.1.4) +struct EdwardsPoint { + Bignum x; + Bignum y; + Bignum z; + Bignum t; +}; + +struct EdwardsParameters { + Bignum prime; + Bignum order; + Bignum coefficient_a; + Bignum coefficient_d; + EdwardsPoint base; +}; + +// Interpret the bytes as a little-endian unsigned integer, the encoding EdDSA +// uses throughout (RFC 8032 Section 5.1.2), by reversing into the big-endian +// conversion +inline auto bignum_from_bytes_little_endian(const std::string_view input) + -> Bignum { + const std::string reversed{input.rbegin(), input.rend()}; + return bignum_from_bytes(reversed); +} + +// The complete unified Edwards addition formulas in extended coordinates +// (Hisil, Wong, Carter, and Dawson 2008), which hold for any two points, +// including equal points and the identity, since the curve coefficient is a +// square and d is a non-square modulo p +inline auto edwards_point_add(const EdwardsPoint &left, + const EdwardsPoint &right, + const EdwardsParameters ¶meters) + -> EdwardsPoint { + const auto &prime{parameters.prime}; + const auto a{bignum_mod_multiply(left.x, right.x, prime)}; + const auto b{bignum_mod_multiply(left.y, right.y, prime)}; + const auto c{bignum_mod_multiply( + bignum_mod_multiply(parameters.coefficient_d, left.t, prime), right.t, + prime)}; + const auto d{bignum_mod_multiply(left.z, right.z, prime)}; + const auto e{bignum_mod_subtract( + bignum_mod_multiply(bignum_mod_add(left.x, left.y, prime), + bignum_mod_add(right.x, right.y, prime), prime), + bignum_mod_add(a, b, prime), prime)}; + const auto f{bignum_mod_subtract(d, c, prime)}; + const auto g{bignum_mod_add(d, c, prime)}; + const auto h{bignum_mod_subtract( + b, bignum_mod_multiply(parameters.coefficient_a, a, prime), prime)}; + return EdwardsPoint{.x = bignum_mod_multiply(e, f, prime), + .y = bignum_mod_multiply(g, h, prime), + .z = bignum_mod_multiply(f, g, prime), + .t = bignum_mod_multiply(e, h, prime)}; +} + +inline auto edwards_point_scalar_multiply(const Bignum &scalar, + const EdwardsPoint &point, + const EdwardsParameters ¶meters) + -> EdwardsPoint { + // The identity element is (0 : 1 : 1 : 0) + EdwardsPoint result{.x = Bignum{}, + .y = bignum_from_u64(1), + .z = bignum_from_u64(1), + .t = Bignum{}}; + const auto bits{bignum_bit_length(scalar)}; + for (std::size_t index = bits; index > 0; --index) { + result = edwards_point_add(result, result, parameters); + if (bignum_get_bit(scalar, index - 1)) { + result = edwards_point_add(result, point, parameters); + } + } + + return result; +} + +// Whether two points are equal, compared without leaving projective space by +// cross-multiplying through the Z factors +inline auto edwards_point_equal(const EdwardsPoint &left, + const EdwardsPoint &right, const Bignum &prime) + -> bool { + return bignum_compare(bignum_mod_multiply(left.x, right.z, prime), + bignum_mod_multiply(right.x, left.z, prime)) == 0 && + bignum_compare(bignum_mod_multiply(left.y, right.z, prime), + bignum_mod_multiply(right.y, left.z, prime)) == 0; +} + +// Recover an Ed25519 point from its 32-byte encoding (RFC 8032 Section 5.1.3), +// returning no value when the encoding does not name a point on the curve +inline auto edwards25519_decode_point(const std::string_view encoding, + const Bignum &prime, + const Bignum &coefficient_d, + const Bignum &square_root_of_minus_one) + -> std::optional { + if (encoding.size() != 32) { + return std::nullopt; + } + + // The final bit holds the sign of x, the remaining bits the little-endian y + std::string bytes{encoding}; + const auto sign_bit{ + static_cast(static_cast(bytes.back()) >> 7) & 1u}; + bytes.back() = + static_cast(static_cast(bytes.back()) & 0x7fu); + const auto y{bignum_from_bytes_little_endian(bytes)}; + + // A y coordinate at or beyond the field prime is not a canonical encoding + if (bignum_compare(y, prime) >= 0) { + return std::nullopt; + } + + const auto one{bignum_from_u64(1)}; + const auto y_squared{bignum_mod_multiply(y, y, prime)}; + + // Solve x^2 = (y^2 - 1) / (d * y^2 + 1) (mod p) + const auto numerator{bignum_mod_subtract(y_squared, one, prime)}; + const auto denominator{bignum_mod_add( + bignum_mod_multiply(coefficient_d, y_squared, prime), one, prime)}; + + // The candidate root is x = numerator * denominator^3 * + // (numerator * denominator^7)^((p - 5) / 8) (mod p), a single powering that + // folds in the inversion of the denominator + const auto denominator_squared{ + bignum_mod_multiply(denominator, denominator, prime)}; + const auto denominator_cubed{ + bignum_mod_multiply(denominator_squared, denominator, prime)}; + const auto denominator_seventh{bignum_mod_multiply( + bignum_mod_multiply(denominator_cubed, denominator_cubed, prime), + denominator, prime)}; + auto exponent{prime}; + bignum_subtract_in_place(exponent, bignum_from_u64(5)); + exponent = bignum_shift_right(exponent, 3); + const auto root{ + bignum_mod_exp(bignum_mod_multiply(numerator, denominator_seventh, prime), + exponent, prime)}; + auto candidate{bignum_mod_multiply( + bignum_mod_multiply(numerator, denominator_cubed, prime), root, prime)}; + + // The candidate is correct when denominator * x^2 equals the numerator, off + // by sqrt(-1) when it equals its negation, and otherwise no root exists + const auto check{bignum_mod_multiply( + denominator, bignum_mod_multiply(candidate, candidate, prime), prime)}; + if (bignum_compare(check, numerator) != 0) { + const auto negated_numerator{ + bignum_mod_subtract(Bignum{}, numerator, prime)}; + if (bignum_compare(check, negated_numerator) != 0) { + return std::nullopt; + } + + candidate = bignum_mod_multiply(candidate, square_root_of_minus_one, prime); + } + + // Reject the non-canonical zero root with a set sign bit, then select the + // root whose low bit matches the encoded sign + if (bignum_is_zero(candidate) && sign_bit == 1) { + return std::nullopt; + } + + if (static_cast(bignum_get_bit(candidate, 0)) != sign_bit) { + auto negated{prime}; + bignum_subtract_in_place(negated, candidate); + candidate = negated; + } + + return EdwardsPoint{.x = candidate, + .y = y, + .z = one, + .t = bignum_mod_multiply(candidate, y, prime)}; +} + +// The Edwards25519 domain parameters (RFC 8032 Section 5.1) +inline auto edwards25519() -> EdwardsParameters { + EdwardsParameters parameters; + parameters.prime = bignum_from_hex( + "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed"); + parameters.order = bignum_from_hex( + "1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed"); + + // The curve coefficient a is -1 (mod p) + parameters.coefficient_a = parameters.prime; + bignum_subtract_in_place(parameters.coefficient_a, bignum_from_u64(1)); + + // d = -121665 / 121666 (mod p) + auto negated_numerator{parameters.prime}; + bignum_subtract_in_place(negated_numerator, bignum_from_u64(121665)); + parameters.coefficient_d = bignum_mod_multiply( + negated_numerator, + bignum_mod_inverse(bignum_from_u64(121666), parameters.prime), + parameters.prime); + + // sqrt(-1) = 2^((p - 1) / 4) (mod p), used to recover the second root + auto root_exponent{parameters.prime}; + bignum_subtract_in_place(root_exponent, bignum_from_u64(1)); + root_exponent = bignum_shift_right(root_exponent, 2); + const auto square_root_of_minus_one{ + bignum_mod_exp(bignum_from_u64(2), root_exponent, parameters.prime)}; + + // The base point is recovered from its canonical encoding, y = 4/5 with a + // clear sign bit (RFC 8032 Section 5.1) + std::string base_encoding; + base_encoding.push_back('\x58'); + base_encoding.append(31, '\x66'); + parameters.base = edwards25519_decode_point(base_encoding, parameters.prime, + parameters.coefficient_d, + square_root_of_minus_one) + .value(); + return parameters; +} + +// Verify an Ed25519 signature over a message (RFC 8032 Section 5.1.7), given +// the 32-byte public key and the 64-byte signature +inline auto edwards25519_verify(const std::string_view public_key, + const std::string_view message, + const std::string_view signature) -> bool { + if (public_key.size() != 32 || signature.size() != 64) { + return false; + } + + const auto parameters{edwards25519()}; + auto square_root_exponent{parameters.prime}; + bignum_subtract_in_place(square_root_exponent, bignum_from_u64(1)); + square_root_exponent = bignum_shift_right(square_root_exponent, 2); + const auto square_root_of_minus_one{bignum_mod_exp( + bignum_from_u64(2), square_root_exponent, parameters.prime)}; + + const auto public_point{edwards25519_decode_point( + public_key, parameters.prime, parameters.coefficient_d, + square_root_of_minus_one)}; + if (!public_point.has_value()) { + return false; + } + + // The signature is the encoded point R followed by the little-endian scalar + // S, which must lie below the group order + const auto encoded_r{signature.substr(0, 32)}; + const auto point_r{edwards25519_decode_point(encoded_r, parameters.prime, + parameters.coefficient_d, + square_root_of_minus_one)}; + if (!point_r.has_value()) { + return false; + } + + const auto scalar_s{bignum_from_bytes_little_endian(signature.substr(32))}; + if (bignum_compare(scalar_s, parameters.order) >= 0) { + return false; + } + + // k = SHA-512(R || A || M) reduced modulo the group order + std::string preimage; + preimage.reserve(encoded_r.size() + public_key.size() + message.size()); + preimage.append(encoded_r); + preimage.append(public_key); + preimage.append(message); + const auto digest{sha512_digest(preimage)}; + auto scalar_k{bignum_from_bytes_little_endian(std::string_view{ + reinterpret_cast(digest.data()), digest.size()})}; + bignum_reduce(scalar_k, parameters.order); + + // The signature holds when [S]B = R + [k]A + const auto left{ + edwards_point_scalar_multiply(scalar_s, parameters.base, parameters)}; + const auto right{edwards_point_add( + point_r.value(), + edwards_point_scalar_multiply(scalar_k, public_point.value(), parameters), + parameters)}; + return edwards_point_equal(left, right, parameters.prime); +} + +// Recover an Ed448 point from its 57-byte encoding (RFC 8032 Section 5.2.3), +// returning no value when the encoding does not name a point on the curve +inline auto edwards448_decode_point(const std::string_view encoding, + const Bignum &prime, + const Bignum &coefficient_d) + -> std::optional { + if (encoding.size() != 57) { + return std::nullopt; + } + + // The final bit holds the sign of x, the remaining bits the little-endian y + std::string bytes{encoding}; + const auto sign_bit{ + static_cast(static_cast(bytes.back()) >> 7) & 1u}; + bytes.back() = + static_cast(static_cast(bytes.back()) & 0x7fu); + const auto y{bignum_from_bytes_little_endian(bytes)}; + + // A y coordinate at or beyond the field prime is not a canonical encoding + if (bignum_compare(y, prime) >= 0) { + return std::nullopt; + } + + const auto one{bignum_from_u64(1)}; + const auto y_squared{bignum_mod_multiply(y, y, prime)}; + + // Solve x^2 = (y^2 - 1) / (d * y^2 - 1) (mod p) + const auto numerator{bignum_mod_subtract(y_squared, one, prime)}; + const auto denominator{bignum_mod_subtract( + bignum_mod_multiply(coefficient_d, y_squared, prime), one, prime)}; + + // The candidate root is x = numerator^3 * denominator * + // (numerator^5 * denominator^3)^((p - 3) / 4) (mod p), the field having + // p congruent to 3 modulo 4 + const auto numerator_squared{ + bignum_mod_multiply(numerator, numerator, prime)}; + const auto numerator_cubed{ + bignum_mod_multiply(numerator_squared, numerator, prime)}; + const auto numerator_fifth{ + bignum_mod_multiply(numerator_squared, numerator_cubed, prime)}; + const auto denominator_squared{ + bignum_mod_multiply(denominator, denominator, prime)}; + const auto denominator_cubed{ + bignum_mod_multiply(denominator_squared, denominator, prime)}; + auto exponent{prime}; + bignum_subtract_in_place(exponent, bignum_from_u64(3)); + exponent = bignum_shift_right(exponent, 2); + const auto root{bignum_mod_exp( + bignum_mod_multiply(numerator_fifth, denominator_cubed, prime), exponent, + prime)}; + auto candidate{bignum_mod_multiply( + bignum_mod_multiply(numerator_cubed, denominator, prime), root, prime)}; + + // The candidate is correct when denominator * x^2 equals the numerator, and + // otherwise no root exists, as the field admits a single square root + const auto check{bignum_mod_multiply( + denominator, bignum_mod_multiply(candidate, candidate, prime), prime)}; + if (bignum_compare(check, numerator) != 0) { + return std::nullopt; + } + + // Reject the non-canonical zero root with a set sign bit, then select the + // root whose low bit matches the encoded sign + if (bignum_is_zero(candidate) && sign_bit == 1) { + return std::nullopt; + } + + if (static_cast(bignum_get_bit(candidate, 0)) != sign_bit) { + auto negated{prime}; + bignum_subtract_in_place(negated, candidate); + candidate = negated; + } + + return EdwardsPoint{.x = candidate, + .y = y, + .z = one, + .t = bignum_mod_multiply(candidate, y, prime)}; +} + +// The Edwards448 domain parameters (RFC 8032 Section 5.2) +inline auto edwards448() -> EdwardsParameters { + EdwardsParameters parameters; + // clang-format off + parameters.prime = bignum_from_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + parameters.order = bignum_from_hex("3fffffffffffffffffffffffffffffffffffffffffffffffffffffff7cca23e9c44edb49aed63690216cc2728dc58f552378c292ab5844f3"); + // clang-format on + + // The curve coefficient a is 1, and d is -39081 (mod p) + parameters.coefficient_a = bignum_from_u64(1); + parameters.coefficient_d = parameters.prime; + bignum_subtract_in_place(parameters.coefficient_d, bignum_from_u64(39081)); + + // The base point is recovered from its canonical 57-octet encoding (RFC 8032 + // Section 5.2) + // clang-format off + const auto base_encoding{bignum_to_bytes(bignum_from_hex("14fa30f25b790898adc8d74e2c13bdfdc4397ce61cffd33ad7c2a0051e9c78874098a36c7373ea4b62c7c9563720768824bcb66e71463f6900"), 57)}; + // clang-format on + parameters.base = edwards448_decode_point(base_encoding, parameters.prime, + parameters.coefficient_d) + .value(); + return parameters; +} + +// Verify an Ed448 signature over a message (RFC 8032 Section 5.2.7), given the +// 57-byte public key and the 114-byte signature +inline auto edwards448_verify(const std::string_view public_key, + const std::string_view message, + const std::string_view signature) -> bool { + if (public_key.size() != 57 || signature.size() != 114) { + return false; + } + + const auto parameters{edwards448()}; + const auto public_point{edwards448_decode_point(public_key, parameters.prime, + parameters.coefficient_d)}; + if (!public_point.has_value()) { + return false; + } + + // The signature is the encoded point R followed by the little-endian scalar + // S, which must lie below the group order + const auto encoded_r{signature.substr(0, 57)}; + const auto point_r{edwards448_decode_point(encoded_r, parameters.prime, + parameters.coefficient_d)}; + if (!point_r.has_value()) { + return false; + } + + const auto scalar_s{bignum_from_bytes_little_endian(signature.substr(57))}; + if (bignum_compare(scalar_s, parameters.order) >= 0) { + return false; + } + + // k = SHAKE256(dom4 || R || A || M) reduced modulo the group order, where + // dom4 is "SigEd448" followed by the zero pre-hash flag and an empty context + // (RFC 8032 Section 5.2.7 and Section 2) + std::string preimage{"SigEd448"}; + preimage.push_back('\x00'); + preimage.push_back('\x00'); + preimage.append(encoded_r); + preimage.append(public_key); + preimage.append(message); + const auto digest{shake256(preimage, 114)}; + auto scalar_k{bignum_from_bytes_little_endian(digest)}; + bignum_reduce(scalar_k, parameters.order); + + // The signature holds when [S]B = R + [k]A + const auto left{ + edwards_point_scalar_multiply(scalar_s, parameters.base, parameters)}; + const auto right{edwards_point_add( + point_r.value(), + edwards_point_scalar_multiply(scalar_k, public_point.value(), parameters), + parameters)}; + return edwards_point_equal(left, right, parameters.prime); +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_eddsa_apple.h b/vendor/core/src/core/crypto/crypto_eddsa_apple.h new file mode 100644 index 00000000..7426649a --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa_apple.h @@ -0,0 +1,15 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_EDDSA_APPLE_H_ +#define SOURCEMETA_CORE_CRYPTO_EDDSA_APPLE_H_ + +#include // std::size_t + +// Verify an Ed25519 signature through CryptoKit, defined in the Objective-C++ +// bridge that consumes the Swift shim. The signature is invalid rather than an +// error for any malformed input, including a key or signature of the wrong +// length, since CryptoKit rejects those inputs +extern "C" auto sourcemeta_core_eddsa_ed25519_verify_cryptokit( + const unsigned char *public_key, std::size_t public_key_size, + const unsigned char *message, std::size_t message_size, + const unsigned char *signature, std::size_t signature_size) -> bool; + +#endif diff --git a/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm new file mode 100644 index 00000000..814b6b50 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.mm @@ -0,0 +1,20 @@ +#include "crypto_eddsa_apple.h" + +#import // NSData + +// The Objective-C interface generated from the Swift shim +#import "sourcemeta_core_cryptokit-Swift.h" + +extern "C" auto sourcemeta_core_eddsa_ed25519_verify_cryptokit( + const unsigned char *public_key, std::size_t public_key_size, + const unsigned char *message, std::size_t message_size, + const unsigned char *signature, std::size_t signature_size) -> bool { + @autoreleasepool { + NSData *const key{[NSData dataWithBytes:public_key length:public_key_size]}; + NSData *const payload{[NSData dataWithBytes:message length:message_size]}; + NSData *const tag{[NSData dataWithBytes:signature length:signature_size]}; + return [SourcemetaCoreEd25519 verifyWithPublicKey:key + message:payload + signature:tag] == YES; + } +} diff --git a/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift new file mode 100644 index 00000000..0ab74c74 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_eddsa_cryptokit.swift @@ -0,0 +1,19 @@ +import CryptoKit +import Foundation + +// The Ed25519 verification primitive that the Apple Security framework does +// not expose through its C API. CryptoKit provides it since macOS 10.15, and +// this class surfaces it to the Objective-C++ bridge through the generated +// Objective-C interface header +@objc(SourcemetaCoreEd25519) +public final class SourcemetaCoreEd25519: NSObject { + @objc public static func verify(publicKey: Data, message: Data, + signature: Data) -> Bool { + guard let key = try? Curve25519.Signing.PublicKey( + rawRepresentation: publicKey) else { + return false + } + + return key.isValidSignature(signature, for: message) + } +} diff --git a/vendor/core/src/core/crypto/crypto_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..e1751d46 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_helpers.h @@ -0,0 +1,133 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_HELPERS_H_ +#define SOURCEMETA_CORE_CRYPTO_HELPERS_H_ + +#include +#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}; + +// Whether a signature representative, as a big-endian integer, is strictly +// less than the modulus. RFC 8017 Section 5.2.2 requires this range check, so +// that an unreduced signature, which an attacker forges by adding the modulus +// without changing the modular exponentiation result, is rejected +inline auto rsa_signature_in_range(const std::string_view signature, + const std::string_view modulus) noexcept + -> bool { + const auto value{strip_left(signature, '\x00')}; + const auto bound{strip_left(modulus, '\x00')}; + if (value.size() != bound.size()) { + return value.size() < bound.size(); + } + + return value < bound; +} + +inline auto curve_field_bytes(const EllipticCurve curve) noexcept + -> std::size_t { + switch (curve) { + case EllipticCurve::P256: + return 32; + case EllipticCurve::P384: + return 48; + case EllipticCurve::P521: + return 66; + } + + std::unreachable(); +} + +// The public key and signature octet lengths are fixed per curve (RFC 8032 +// Section 5.1.2 and Section 5.1.6) +inline auto eddsa_public_key_bytes(const EdwardsCurve curve) noexcept + -> std::size_t { + switch (curve) { + case EdwardsCurve::Ed25519: + return 32; + case EdwardsCurve::Ed448: + return 57; + } + + std::unreachable(); +} + +inline auto eddsa_signature_bytes(const EdwardsCurve curve) noexcept + -> std::size_t { + switch (curve) { + case EdwardsCurve::Ed25519: + return 64; + case EdwardsCurve::Ed448: + return 114; + } + + std::unreachable(); +} + +inline auto digest_message(const SignatureHashFunction hash, + const std::string_view message) -> std::string { + switch (hash) { + 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_shake256.h b/vendor/core/src/core/crypto/crypto_shake256.h new file mode 100644 index 00000000..dd942542 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_shake256.h @@ -0,0 +1,131 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_SHAKE256_H_ +#define SOURCEMETA_CORE_CRYPTO_SHAKE256_H_ + +// The SHAKE256 extendable-output function (FIPS 202), built on the Keccak-f +// [1600] permutation, for the reference Ed448 verification backend + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint64_t +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +inline constexpr std::array keccak_round_constants{ + {0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, + 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL}}; + +// The rotation offsets and destination lanes of the combined rho and pi steps, +// walking the lanes starting from lane 1 +inline constexpr std::array keccak_rho_offsets{ + {1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, + 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44}}; + +inline constexpr std::array keccak_pi_lanes{ + {10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, + 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1}}; + +inline constexpr auto keccak_rotate_left(const std::uint64_t value, + const unsigned offset) noexcept + -> std::uint64_t { + return (value << offset) | (value >> (64u - offset)); +} + +inline auto keccak_permute(std::array &state) noexcept + -> void { + for (std::size_t round = 0; round < 24; ++round) { + // Theta + std::array column_parity{}; + for (std::size_t column = 0; column < 5; ++column) { + column_parity[column] = state[column] ^ state[column + 5] ^ + state[column + 10] ^ state[column + 15] ^ + state[column + 20]; + } + + for (std::size_t column = 0; column < 5; ++column) { + const auto delta{column_parity[(column + 4) % 5] ^ + keccak_rotate_left(column_parity[(column + 1) % 5], 1)}; + for (std::size_t row = 0; row < 25; row += 5) { + state[row + column] ^= delta; + } + } + + // Rho and pi + auto current{state[1]}; + for (std::size_t index = 0; index < 24; ++index) { + const auto lane{keccak_pi_lanes[index]}; + const auto moved{state[lane]}; + state[lane] = keccak_rotate_left(current, keccak_rho_offsets[index]); + current = moved; + } + + // Chi + for (std::size_t row = 0; row < 25; row += 5) { + std::array plane{}; + for (std::size_t column = 0; column < 5; ++column) { + plane[column] = state[row + column]; + } + + for (std::size_t column = 0; column < 5; ++column) { + state[row + column] = plane[column] ^ (~plane[(column + 1) % 5] & + plane[(column + 2) % 5]); + } + } + + // Iota + state[0] ^= keccak_round_constants[round]; + } +} + +// Hash a string with SHAKE256, returning the requested number of output bytes +inline auto shake256(const std::string_view input, + const std::size_t output_length) -> std::string { + // The bitrate is 1600 - 2 * 256 = 1088 bits, that is 136 octets + constexpr std::size_t rate{136}; + std::array state{}; + + std::size_t pointer{0}; + for (const auto character : input) { + state[pointer / 8] ^= + static_cast(static_cast(character)) + << (8 * (pointer % 8)); + pointer += 1; + if (pointer == rate) { + keccak_permute(state); + pointer = 0; + } + } + + // The SHAKE domain separation suffix is the bits 1111, padded to the rate + // with the pad10*1 rule + state[pointer / 8] ^= static_cast(0x1f) << (8 * (pointer % 8)); + state[(rate - 1) / 8] ^= static_cast(0x80) + << (8 * ((rate - 1) % 8)); + keccak_permute(state); + + std::string output; + output.reserve(output_length); + std::size_t squeeze_pointer{0}; + while (output.size() < output_length) { + output.push_back(static_cast( + (state[squeeze_pointer / 8] >> (8 * (squeeze_pointer % 8))) & 0xffu)); + squeeze_pointer += 1; + if (squeeze_pointer == rate) { + keccak_permute(state); + squeeze_pointer = 0; + } + } + + return output; +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/crypto/crypto_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_apple.cc b/vendor/core/src/core/crypto/crypto_verify_apple.cc new file mode 100644 index 00000000..7f116b0f --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_apple.cc @@ -0,0 +1,368 @@ +#include +#include + +#include "crypto_eddsa.h" +#include "crypto_eddsa_apple.h" +#include "crypto_helpers.h" + +#include // CF*, kCF* +#include // Sec*, kSec* + +#include // std::array +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { + +// The parsed key keeps the platform key object alive for reuse. The Edwards +// curves have no Security framework primitive, so they keep the raw encoded +// point and verify through CryptoKit or the reference implementation +struct PublicKey::Internal { + PublicKey::Type kind; + SecKeyRef key; + std::string modulus; + std::size_t field_bytes; + std::string edwards_point; + EdwardsCurve edwards_curve; +}; + +} // namespace sourcemeta::core + +namespace { + +auto to_sec_key_pkcs1_v15_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> SecKeyAlgorithm { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256; + case sourcemeta::core::SignatureHashFunction::SHA384: + return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA384; + case sourcemeta::core::SignatureHashFunction::SHA512: + return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA512; + } + + std::unreachable(); +} + +// These algorithm variants fix the salt length to the hash function output, +// which is exactly what RFC 7518 Section 3.5 requires +auto to_sec_key_pss_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> SecKeyAlgorithm { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return kSecKeyAlgorithmRSASignatureMessagePSSSHA256; + case sourcemeta::core::SignatureHashFunction::SHA384: + return kSecKeyAlgorithmRSASignatureMessagePSSSHA384; + case sourcemeta::core::SignatureHashFunction::SHA512: + return kSecKeyAlgorithmRSASignatureMessagePSSSHA512; + } + + std::unreachable(); +} + +auto to_sec_key_ecdsa_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> SecKeyAlgorithm { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return kSecKeyAlgorithmECDSASignatureMessageX962SHA256; + case sourcemeta::core::SignatureHashFunction::SHA384: + return kSecKeyAlgorithmECDSASignatureMessageX962SHA384; + case sourcemeta::core::SignatureHashFunction::SHA512: + return kSecKeyAlgorithmECDSASignatureMessageX962SHA512; + } + + std::unreachable(); +} + +auto make_data(const std::string_view value) -> CFDataRef { + return CFDataCreate(kCFAllocatorDefault, + reinterpret_cast(value.data()), + static_cast(value.size())); +} + +auto native_rsa_key(const std::string_view modulus, + const std::string_view exponent) -> SecKeyRef { + // The platform expects the PKCS#1 RSAPublicKey structure, a DER sequence of + // the modulus and exponent integers (RFC 8017 Appendix A.1.1) + std::string body; + sourcemeta::core::der_append_unsigned_integer(body, modulus); + sourcemeta::core::der_append_unsigned_integer(body, exponent); + std::string der; + der.push_back('\x30'); + sourcemeta::core::der_append_length(der, body.size()); + der.append(body); + + auto key_data{make_data(der)}; + if (key_data == nullptr) { + return nullptr; + } + + std::array attribute_keys{ + {kSecAttrKeyType, kSecAttrKeyClass}}; + std::array attribute_values{ + {kSecAttrKeyTypeRSA, kSecAttrKeyClassPublic}}; + auto attributes{CFDictionaryCreate( + kCFAllocatorDefault, attribute_keys.data(), attribute_values.data(), + static_cast(attribute_keys.size()), + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)}; + if (attributes == nullptr) { + CFRelease(key_data); + return nullptr; + } + + auto key{SecKeyCreateWithData(key_data, attributes, nullptr)}; + CFRelease(attributes); + CFRelease(key_data); + return key; +} + +auto native_ec_key(const sourcemeta::core::EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) -> SecKeyRef { + const auto width{sourcemeta::core::curve_field_bytes(curve)}; + const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; + const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > width || stripped_y.size() > width) { + return nullptr; + } + + // The platform infers the curve from the X9.63 uncompressed point, the 0x04 + // lead byte followed by the two fixed-width coordinates + std::string point; + point.push_back('\x04'); + point.append(sourcemeta::core::pad_left(stripped_x, width, '\x00')); + point.append(sourcemeta::core::pad_left(stripped_y, width, '\x00')); + + auto key_data{make_data(point)}; + if (key_data == nullptr) { + return nullptr; + } + + std::array attribute_keys{ + {kSecAttrKeyType, kSecAttrKeyClass}}; + std::array attribute_values{ + {kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClassPublic}}; + auto attributes{CFDictionaryCreate( + kCFAllocatorDefault, attribute_keys.data(), attribute_values.data(), + static_cast(attribute_keys.size()), + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)}; + if (attributes == nullptr) { + CFRelease(key_data); + return nullptr; + } + + auto key{SecKeyCreateWithData(key_data, attributes, nullptr)}; + CFRelease(attributes); + CFRelease(key_data); + return key; +} + +auto encode_ecdsa_signature(const std::string_view raw_signature) + -> std::string { + // The raw form is the two integers concatenated, while the platform expects + // the X9.62 DER sequence of those integers + const auto half{raw_signature.size() / 2}; + std::string body; + sourcemeta::core::der_append_unsigned_integer(body, + raw_signature.substr(0, half)); + sourcemeta::core::der_append_unsigned_integer(body, + raw_signature.substr(half)); + std::string der; + der.push_back('\x30'); + sourcemeta::core::der_append_length(der, body.size()); + der.append(body); + return der; +} + +auto verify_with_algorithm(SecKeyRef key, const SecKeyAlgorithm algorithm, + const std::string_view message, + const std::string_view signature) -> bool { + auto message_data{make_data(message)}; + auto signature_data{make_data(signature)}; + auto result{false}; + if (message_data != nullptr && signature_data != nullptr) { + result = SecKeyVerifySignature(key, algorithm, message_data, signature_data, + nullptr) == true; + } + + if (signature_data != nullptr) { + CFRelease(signature_data); + } + + if (message_data != nullptr) { + CFRelease(message_data); + } + + return result; +} + +} // namespace + +namespace sourcemeta::core { + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + CFRelease(internal_->key); + } + + delete internal_; + } +} + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + CFRelease(internal_->key); + } + + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { return internal_->kind; } + +auto make_rsa_public_key(const std::string_view modulus, + const std::string_view exponent) + -> std::optional { + auto stripped_modulus{std::string{strip_left(modulus, '\x00')}}; + const auto stripped_exponent{strip_left(exponent, '\x00')}; + if (stripped_modulus.empty() || stripped_exponent.empty() || + stripped_modulus.size() > MAXIMUM_KEY_BYTES || + stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + return std::nullopt; + } + + auto *key{native_rsa_key(stripped_modulus, stripped_exponent)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .key = key, + .modulus = std::move(stripped_modulus), + .field_bytes = 0, + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + auto *key{native_ec_key(curve, coordinate_x, coordinate_y)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .key = key, + .modulus = {}, + .field_bytes = curve_field_bytes(curve), + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .key = nullptr, + .modulus = {}, + .field_bytes = 0, + .edwards_point = std::string{public_key}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_with_algorithm( + internal->key, to_sec_key_pkcs1_v15_algorithm(hash), message, signature); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_with_algorithm(internal->key, to_sec_key_pss_algorithm(hash), + message, signature); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::EllipticCurve || + signature.size() != internal->field_bytes * 2) { + return false; + } + + return verify_with_algorithm(internal->key, to_sec_key_ecdsa_algorithm(hash), + message, encode_ecdsa_signature(signature)); +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards || + signature.size() != eddsa_signature_bytes(internal->edwards_curve)) { + return false; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: + return sourcemeta_core_eddsa_ed25519_verify_cryptokit( + reinterpret_cast( + internal->edwards_point.data()), + internal->edwards_point.size(), + reinterpret_cast(message.data()), + message.size(), + reinterpret_cast(signature.data()), + signature.size()); + case EdwardsCurve::Ed448: + return edwards448_verify(internal->edwards_point, message, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_openssl.cc b/vendor/core/src/core/crypto/crypto_verify_openssl.cc new file mode 100644 index 00000000..428ba2f1 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_openssl.cc @@ -0,0 +1,404 @@ +#include +#include + +#include "crypto_helpers.h" + +#include // BN_* +#include // OSSL_PKEY_PARAM_* +#include // ECDSA_SIG_*, i2d_ECDSA_SIG +#include // EVP_* +#include // OSSL_PARAM_* +#include // RSA_PKCS1_PSS_PADDING, RSA_PSS_SALTLEN_DIGEST + +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { + +// The parsed key keeps the native handle alive so that many signatures verify +// without rebuilding it +struct PublicKey::Internal { + PublicKey::Type kind; + EVP_PKEY *key; + // The stripped modulus, kept for the RSA signature range check + std::string modulus; + // The field width for the elliptic curve signature size check + std::size_t field_bytes; + // The expected signature length for the Edwards curve + std::size_t signature_bytes; +}; + +} // namespace sourcemeta::core + +namespace { + +auto to_message_digest( + const sourcemeta::core::SignatureHashFunction hash) noexcept + -> const EVP_MD * { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return EVP_sha256(); + case sourcemeta::core::SignatureHashFunction::SHA384: + return EVP_sha384(); + case sourcemeta::core::SignatureHashFunction::SHA512: + return EVP_sha512(); + } + + std::unreachable(); +} + +auto to_group_name(const sourcemeta::core::EllipticCurve curve) noexcept + -> const char * { + switch (curve) { + case sourcemeta::core::EllipticCurve::P256: + return "P-256"; + case sourcemeta::core::EllipticCurve::P384: + return "P-384"; + case sourcemeta::core::EllipticCurve::P521: + return "P-521"; + } + + std::unreachable(); +} + +auto to_pkey_id(const sourcemeta::core::EdwardsCurve curve) noexcept -> int { + switch (curve) { + case sourcemeta::core::EdwardsCurve::Ed25519: + return EVP_PKEY_ED25519; + case sourcemeta::core::EdwardsCurve::Ed448: + return EVP_PKEY_ED448; + } + + std::unreachable(); +} + +auto native_rsa_key(const std::string_view modulus, + const std::string_view exponent) -> EVP_PKEY * { + EVP_PKEY *result{nullptr}; + auto *modulus_number{ + BN_bin2bn(reinterpret_cast(modulus.data()), + static_cast(modulus.size()), nullptr)}; + auto *exponent_number{ + BN_bin2bn(reinterpret_cast(exponent.data()), + static_cast(exponent.size()), nullptr)}; + auto *builder{OSSL_PARAM_BLD_new()}; + + if (modulus_number != nullptr && exponent_number != nullptr && + builder != nullptr && + OSSL_PARAM_BLD_push_BN(builder, OSSL_PKEY_PARAM_RSA_N, modulus_number) == + 1 && + OSSL_PARAM_BLD_push_BN(builder, OSSL_PKEY_PARAM_RSA_E, exponent_number) == + 1) { + auto *parameters{OSSL_PARAM_BLD_to_param(builder)}; + if (parameters != nullptr) { + auto *context{EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr)}; + if (context != nullptr) { + if (EVP_PKEY_fromdata_init(context) == 1) { + EVP_PKEY_fromdata(context, &result, EVP_PKEY_PUBLIC_KEY, parameters); + } + + EVP_PKEY_CTX_free(context); + } + + OSSL_PARAM_free(parameters); + } + } + + OSSL_PARAM_BLD_free(builder); + BN_free(exponent_number); + BN_free(modulus_number); + return result; +} + +auto native_ec_key(const sourcemeta::core::EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) -> EVP_PKEY * { + const auto width{sourcemeta::core::curve_field_bytes(curve)}; + const auto stripped_x{sourcemeta::core::strip_left(coordinate_x, '\x00')}; + const auto stripped_y{sourcemeta::core::strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > width || stripped_y.size() > width) { + return nullptr; + } + + std::string point; + point.push_back('\x04'); + point.append(sourcemeta::core::pad_left(stripped_x, width, '\x00')); + point.append(sourcemeta::core::pad_left(stripped_y, width, '\x00')); + + EVP_PKEY *result{nullptr}; + auto *builder{OSSL_PARAM_BLD_new()}; + if (builder != nullptr && + OSSL_PARAM_BLD_push_utf8_string(builder, OSSL_PKEY_PARAM_GROUP_NAME, + to_group_name(curve), 0) == 1 && + OSSL_PARAM_BLD_push_octet_string( + builder, OSSL_PKEY_PARAM_PUB_KEY, + reinterpret_cast(point.data()), + point.size()) == 1) { + auto *parameters{OSSL_PARAM_BLD_to_param(builder)}; + if (parameters != nullptr) { + auto *context{EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr)}; + if (context != nullptr) { + if (EVP_PKEY_fromdata_init(context) == 1) { + EVP_PKEY_fromdata(context, &result, EVP_PKEY_PUBLIC_KEY, parameters); + } + + EVP_PKEY_CTX_free(context); + } + + OSSL_PARAM_free(parameters); + } + } + + OSSL_PARAM_BLD_free(builder); + return result; +} + +// Convert the raw fixed-width R || S concatenation into the DER signature that +// the verification interface expects +auto encode_ecdsa_signature(const std::string_view raw_signature, + unsigned char **output) -> int { + const auto half{raw_signature.size() / 2}; + auto *signature{ECDSA_SIG_new()}; + if (signature == nullptr) { + return -1; + } + + auto *r{ + BN_bin2bn(reinterpret_cast(raw_signature.data()), + static_cast(half), nullptr)}; + auto *s{BN_bin2bn( + reinterpret_cast(raw_signature.data() + half), + static_cast(half), nullptr)}; + if (r == nullptr || s == nullptr || ECDSA_SIG_set0(signature, r, s) != 1) { + BN_free(r); + BN_free(s); + ECDSA_SIG_free(signature); + return -1; + } + + const auto length{i2d_ECDSA_SIG(signature, output)}; + ECDSA_SIG_free(signature); + return length; +} + +auto verify_rsa(EVP_PKEY *key, + const sourcemeta::core::SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature, const bool probabilistic) + -> bool { + auto result{false}; + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + EVP_PKEY_CTX *key_context{nullptr}; + auto ready{EVP_DigestVerifyInit(context, &key_context, + to_message_digest(hash), nullptr, + key) == 1}; + if (ready && probabilistic) { + ready = EVP_PKEY_CTX_set_rsa_padding(key_context, + RSA_PKCS1_PSS_PADDING) == 1 && + EVP_PKEY_CTX_set_rsa_pss_saltlen(key_context, + RSA_PSS_SALTLEN_DIGEST) == 1; + } + + if (ready) { + result = EVP_DigestVerify( + context, + reinterpret_cast(signature.data()), + signature.size(), + reinterpret_cast(message.data()), + message.size()) == 1; + } + + EVP_MD_CTX_free(context); + } + + return result; +} + +} // namespace + +namespace sourcemeta::core { + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { + if (internal_ != nullptr) { + EVP_PKEY_free(internal_->key); + delete internal_; + } +} + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + if (internal_ != nullptr) { + EVP_PKEY_free(internal_->key); + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { return internal_->kind; } + +auto make_rsa_public_key(const std::string_view modulus, + const std::string_view exponent) + -> std::optional { + auto stripped_modulus{std::string{strip_left(modulus, '\x00')}}; + const auto stripped_exponent{strip_left(exponent, '\x00')}; + if (stripped_modulus.empty() || stripped_exponent.empty() || + stripped_modulus.size() > MAXIMUM_KEY_BYTES || + stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + return std::nullopt; + } + + auto *key{native_rsa_key(stripped_modulus, stripped_exponent)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .key = key, + .modulus = std::move(stripped_modulus), + .field_bytes = 0, + .signature_bytes = 0}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + auto *key{native_ec_key(curve, coordinate_x, coordinate_y)}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .key = key, + .modulus = {}, + .field_bytes = curve_field_bytes(curve), + .signature_bytes = 0}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + auto *key{EVP_PKEY_new_raw_public_key( + to_pkey_id(curve), nullptr, + reinterpret_cast(public_key.data()), + public_key.size())}; + if (key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .key = key, + .modulus = {}, + .field_bytes = 0, + .signature_bytes = eddsa_signature_bytes(curve)}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, false); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, true); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::EllipticCurve || + signature.size() != internal->field_bytes * 2) { + return false; + } + + unsigned char *der_signature{nullptr}; + const auto der_length{encode_ecdsa_signature(signature, &der_signature)}; + auto result{false}; + if (der_length > 0) { + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + if (EVP_DigestVerifyInit(context, nullptr, to_message_digest(hash), + nullptr, internal->key) == 1) { + result = + EVP_DigestVerify( + context, der_signature, static_cast(der_length), + reinterpret_cast(message.data()), + message.size()) == 1; + } + + EVP_MD_CTX_free(context); + } + } + + OPENSSL_free(der_signature); + return result; +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards || + signature.size() != internal->signature_bytes) { + return false; + } + + auto result{false}; + auto *context{EVP_MD_CTX_new()}; + if (context != nullptr) { + // EdDSA is a one-shot verification with a null digest, since the curve + // fixes the hash function internally + if (EVP_DigestVerifyInit(context, nullptr, nullptr, nullptr, + internal->key) == 1) { + result = EVP_DigestVerify( + context, + reinterpret_cast(signature.data()), + signature.size(), + reinterpret_cast(message.data()), + message.size()) == 1; + } + + EVP_MD_CTX_free(context); + } + + return result; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_other.cc b/vendor/core/src/core/crypto/crypto_verify_other.cc new file mode 100644 index 00000000..c441bab4 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_other.cc @@ -0,0 +1,478 @@ +#include +#include + +#include "crypto_bignum.h" +#include "crypto_ecc.h" +#include "crypto_eddsa.h" +#include "crypto_helpers.h" + +#include // std::array +#include // std::size_t +#include // std::uint8_t, std::uint32_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::unreachable + +namespace sourcemeta::core { +namespace { + +// The DigestInfo prefixes for the EMSA-PKCS1-v1_5 encoding, taken verbatim +// from RFC 8017 Section 9.2 Note 1 +constexpr std::array DIGEST_INFO_SHA256{ + {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}}; +constexpr std::array DIGEST_INFO_SHA384{ + {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30}}; +constexpr std::array DIGEST_INFO_SHA512{ + {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, + 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40}}; + +auto digest_info_prefix(const SignatureHashFunction hash) -> std::string_view { + switch (hash) { + case SignatureHashFunction::SHA256: + return {reinterpret_cast(DIGEST_INFO_SHA256.data()), + DIGEST_INFO_SHA256.size()}; + case SignatureHashFunction::SHA384: + return {reinterpret_cast(DIGEST_INFO_SHA384.data()), + DIGEST_INFO_SHA384.size()}; + case SignatureHashFunction::SHA512: + return {reinterpret_cast(DIGEST_INFO_SHA512.data()), + DIGEST_INFO_SHA512.size()}; + } + + std::unreachable(); +} + +// EMSA-PKCS1-v1_5 encoding (RFC 8017 Section 9.2) +auto build_encoded_message(const SignatureHashFunction hash, + const std::string_view message, + const std::size_t key_length) + -> std::optional { + const auto prefix{digest_info_prefix(hash)}; + const auto digest{digest_message(hash, message)}; + const auto encoded_length{prefix.size() + digest.size()}; + + // RFC 8017 Section 9.2 step 3: "If emLen < tLen + 11, output 'intended + // encoded message length too short'" + if (key_length < encoded_length + 11) { + return std::nullopt; + } + + std::string result; + result.reserve(key_length); + result.push_back('\x00'); + result.push_back('\x01'); + result.append(key_length - encoded_length - 3, '\xff'); + result.push_back('\x00'); + result.append(prefix); + result.append(digest); + return result; +} + +// MGF1 mask generation (RFC 8017 Appendix B.2.1) +auto mask_generation(const SignatureHashFunction hash, + const std::string_view seed, const std::size_t length) + -> std::string { + std::string result; + result.reserve(length + 64); + std::uint32_t counter{0}; + while (result.size() < length) { + std::string block{seed}; + block.push_back(static_cast((counter >> 24u) & 0xffu)); + block.push_back(static_cast((counter >> 16u) & 0xffu)); + block.push_back(static_cast((counter >> 8u) & 0xffu)); + block.push_back(static_cast(counter & 0xffu)); + result.append(digest_message(hash, block)); + counter += 1; + } + + result.resize(length); + return result; +} + +// EMSA-PSS verification (RFC 8017 Section 9.1.2), with the salt length fixed to +// the hash function output as RFC 7518 Section 3.5 requires +auto emsa_pss_verify(const SignatureHashFunction hash, + const std::string_view message, + const std::string_view encoded_message, + const std::size_t encoded_bits) -> bool { + const auto digest{digest_message(hash, message)}; + const auto hash_length{digest.size()}; + const auto salt_length{hash_length}; + const auto encoded_length{encoded_message.size()}; + + // RFC 8017 Section 9.1.2 step 3: "If emLen < hLen + sLen + 2, output + // 'inconsistent'" + if (encoded_length < hash_length + salt_length + 2) { + return false; + } + + // RFC 8017 Section 9.1.2 step 4: "If the rightmost octet of EM does not have + // hexadecimal value 0xbc, output 'inconsistent'" + if (static_cast(encoded_message.back()) != 0xbc) { + return false; + } + + const auto database_length{encoded_length - hash_length - 1}; + const auto masked_database{encoded_message.substr(0, database_length)}; + const auto hash_value{encoded_message.substr(database_length, hash_length)}; + + // RFC 8017 Section 9.1.2 step 6: "If the leftmost 8emLen - emBits bits of the + // leftmost octet in maskedDB are not all equal to zero, output + // 'inconsistent'" + const auto unused_bits{(8 * encoded_length) - encoded_bits}; + const auto unused_mask{ + static_cast((0xff00u >> unused_bits) & 0xffu)}; + if ((static_cast(masked_database.front()) & unused_mask) != 0) { + return false; + } + + auto database{mask_generation(hash, hash_value, database_length)}; + for (std::size_t index = 0; index < database_length; ++index) { + database[index] = + static_cast(database[index] ^ masked_database[index]); + } + + database[0] = static_cast(static_cast(database[0]) & + static_cast(~unused_mask)); + + // RFC 8017 Section 9.1.2 step 10: "If the emLen - hLen - sLen - 2 leftmost + // octets of DB are not zero or if the octet at position emLen - hLen - sLen - + // 1 does not have hexadecimal value 0x01, output 'inconsistent'" + const auto padding_length{encoded_length - hash_length - salt_length - 2}; + for (std::size_t index = 0; index < padding_length; ++index) { + if (database[index] != '\x00') { + return false; + } + } + + if (static_cast(database[padding_length]) != 0x01) { + return false; + } + + // RFC 8017 Section 9.1.2 steps 12 and 13: hash the concatenation of eight + // zero octets, the message digest, and the recovered salt + std::string verification_input(8, '\x00'); + verification_input.append(digest); + verification_input.append(database.substr(database_length - salt_length)); + const auto expected{digest_message(hash, verification_input)}; + return expected == hash_value; +} + +auto to_curve_parameters(const EllipticCurve curve) -> EllipticCurveParameters { + switch (curve) { + case EllipticCurve::P256: + return curve_p256(); + case EllipticCurve::P384: + return curve_p384(); + case EllipticCurve::P521: + return curve_p521(); + } + + std::unreachable(); +} + +// FIPS 186-4 Section 6.4 step 2, deriving the integer e from the leftmost bits +// of the message digest, truncated to the bit length of the order +auto digest_to_integer(const SignatureHashFunction hash, + const std::string_view message, + const std::size_t order_bits) -> Bignum { + const auto digest{digest_message(hash, message)}; + auto value{bignum_from_bytes(digest)}; + const auto digest_bits{digest.size() * 8}; + if (digest_bits > order_bits) { + value = bignum_shift_right(value, digest_bits - order_bits); + } + + return value; +} + +// RSASSA-PKCS1-v1_5 verification (RFC 8017 Section 8.2.2) over raw key material +auto verify_pkcs1(const SignatureHashFunction hash, + const std::string_view modulus, + const std::string_view exponent, + const std::string_view message, + const std::string_view signature) -> bool { + // RFC 8017 Section 8.2.2 step 1: "If the length of S is not k octets, output + // 'invalid signature'" + const auto key_length{modulus.size()}; + if (signature.size() != key_length) { + return false; + } + + const auto modulus_number{bignum_from_bytes(modulus)}; + const auto signature_number{bignum_from_bytes(signature)}; + + // RFC 8017 Section 5.2.2: "If the signature representative s is not between 0 + // and n - 1, output 'signature representative out of range'" + if (bignum_compare(signature_number, modulus_number) >= 0) { + return false; + } + + const auto exponent_number{bignum_from_bytes(exponent)}; + const auto message_representative{ + bignum_mod_exp(signature_number, exponent_number, modulus_number)}; + const auto encoded_message{ + bignum_to_bytes(message_representative, key_length)}; + const auto expected{build_encoded_message(hash, message, key_length)}; + return expected.has_value() && encoded_message == expected.value(); +} + +// RSASSA-PSS verification (RFC 8017 Section 8.1.2) over raw key material +auto verify_pss(const SignatureHashFunction hash, + const std::string_view modulus, const std::string_view exponent, + const std::string_view message, + const std::string_view signature) -> bool { + // RFC 8017 Section 8.1.2 step 1: "If the length of the signature S is not k + // octets, output 'invalid signature'" + const auto key_length{modulus.size()}; + if (signature.size() != key_length) { + return false; + } + + const auto modulus_number{bignum_from_bytes(modulus)}; + const auto signature_number{bignum_from_bytes(signature)}; + + // RFC 8017 Section 5.2.2: "If the signature representative s is not between 0 + // and n - 1, output 'signature representative out of range'" + if (bignum_compare(signature_number, modulus_number) >= 0) { + return false; + } + + const auto exponent_number{bignum_from_bytes(exponent)}; + const auto message_representative{ + bignum_mod_exp(signature_number, exponent_number, modulus_number)}; + + // RFC 8017 Section 8.1.2 step 2c: the encoded message is emLen octets long, + // where emLen equals the byte length of emBits = modBits - 1 bits, which is + // one octet less than k when the modulus bit length is congruent to one + // modulo eight + const auto encoded_bits{bignum_bit_length(modulus_number) - 1}; + const auto encoded_length{(encoded_bits + 7) / 8}; + const auto full_representative{ + bignum_to_bytes(message_representative, key_length)}; + for (std::size_t index = 0; index < key_length - encoded_length; ++index) { + if (full_representative[index] != '\x00') { + return false; + } + } + + const auto encoded_message{std::string_view{full_representative}.substr( + key_length - encoded_length)}; + return emsa_pss_verify(hash, message, encoded_message, encoded_bits); +} + +// ECDSA verification (FIPS 186-4 Section 6.4) over the raw public point +auto verify_ecdsa(const EllipticCurve curve, const SignatureHashFunction hash, + const std::string_view coordinate_x, + const std::string_view coordinate_y, + const std::string_view message, + const std::string_view signature) -> bool { + const auto parameters{to_curve_parameters(curve)}; + const auto field_bytes{parameters.field_bytes}; + + // RFC 7518 Section 3.4: the signature is the fixed-width concatenation of the + // two integers, each as long as the curve field + if (signature.size() != field_bytes * 2) { + return false; + } + + const auto r{bignum_from_bytes(signature.substr(0, field_bytes))}; + const auto s{bignum_from_bytes(signature.substr(field_bytes))}; + + // FIPS 186-4 Section 6.4.2 step 1: both integers must lie in [1, n - 1] + if (bignum_is_zero(r) || bignum_compare(r, parameters.order) >= 0 || + bignum_is_zero(s) || bignum_compare(s, parameters.order) >= 0) { + return false; + } + + // Reject coordinates wider than the field, matching the platform backends and + // preventing an oversized input from being truncated into a valid key + const auto stripped_x{strip_left(coordinate_x, '\x00')}; + const auto stripped_y{strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > field_bytes || stripped_y.size() > field_bytes) { + return false; + } + + const auto public_x{bignum_from_bytes(stripped_x)}; + const auto public_y{bignum_from_bytes(stripped_y)}; + + // The public key must be a valid point: coordinates below the field prime and + // satisfying the curve equation + if (bignum_compare(public_x, parameters.prime) >= 0 || + bignum_compare(public_y, parameters.prime) >= 0 || + !point_on_curve(public_x, public_y, parameters)) { + return false; + } + + const auto order_bits{bignum_bit_length(parameters.order)}; + const auto digest_integer{digest_to_integer(hash, message, order_bits)}; + const auto s_inverse{bignum_mod_inverse(s, parameters.order)}; + const auto u1{ + bignum_mod_multiply(digest_integer, s_inverse, parameters.order)}; + const auto u2{bignum_mod_multiply(r, s_inverse, parameters.order)}; + + const JacobianPoint generator{.x = parameters.generator_x, + .y = parameters.generator_y, + .z = bignum_from_u64(1)}; + const JacobianPoint public_point{ + .x = public_x, .y = public_y, .z = bignum_from_u64(1)}; + const auto point{point_double_scalar_multiply(u1, generator, u2, public_point, + parameters)}; + + // FIPS 186-4 Section 6.4.2 step 6: reject when the combination is the point + // at infinity + if (point_is_infinity(point)) { + return false; + } + + // FIPS 186-4 Section 6.4.2 step 7: the signature is valid when the affine x + // coordinate, reduced modulo the order, equals r + auto candidate{point_affine_x(point, parameters)}; + bignum_reduce(candidate, parameters.order); + return bignum_compare(candidate, r) == 0; +} + +} // namespace + +// The reference backend parses the key material into big integers inside each +// verification, which is cheap next to the modular arithmetic, so the parsed +// key simply holds the raw material +struct PublicKey::Internal { + PublicKey::Type kind; + std::string modulus; + std::string exponent; + std::string coordinate_x; + std::string coordinate_y; + EllipticCurve elliptic_curve; + EdwardsCurve edwards_curve; +}; + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { delete internal_; } + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + delete internal_; + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { return internal_->kind; } + +auto make_rsa_public_key(const std::string_view modulus, + const std::string_view exponent) + -> std::optional { + const auto stripped_modulus{strip_left(modulus, '\x00')}; + const auto stripped_exponent{strip_left(exponent, '\x00')}; + if (stripped_modulus.empty() || stripped_exponent.empty() || + stripped_modulus.size() > MAXIMUM_KEY_BYTES || + stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .modulus = std::string{stripped_modulus}, + .exponent = std::string{stripped_exponent}, + .coordinate_x = {}, + .coordinate_y = {}, + .elliptic_curve = {}, + .edwards_curve = {}}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + const auto width{curve_field_bytes(curve)}; + const auto stripped_x{strip_left(coordinate_x, '\x00')}; + const auto stripped_y{strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > width || stripped_y.size() > width) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .modulus = {}, + .exponent = {}, + .coordinate_x = std::string{stripped_x}, + .coordinate_y = std::string{stripped_y}, + .elliptic_curve = curve, + .edwards_curve = {}}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .modulus = {}, + .exponent = {}, + .coordinate_x = std::string{public_key}, + .coordinate_y = {}, + .elliptic_curve = {}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + return internal != nullptr && internal->kind == PublicKey::Type::RSA && + verify_pkcs1(hash, internal->modulus, internal->exponent, message, + signature); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + return internal != nullptr && internal->kind == PublicKey::Type::RSA && + verify_pss(hash, internal->modulus, internal->exponent, message, + signature); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + return internal != nullptr && + internal->kind == PublicKey::Type::EllipticCurve && + verify_ecdsa(internal->elliptic_curve, hash, internal->coordinate_x, + internal->coordinate_y, message, signature); +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards) { + return false; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: + return edwards25519_verify(internal->coordinate_x, message, signature); + case EdwardsCurve::Ed448: + return edwards448_verify(internal->coordinate_x, message, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/crypto_verify_windows.cc b/vendor/core/src/core/crypto/crypto_verify_windows.cc new file mode 100644 index 00000000..0e41c9e1 --- /dev/null +++ b/vendor/core/src/core/crypto/crypto_verify_windows.cc @@ -0,0 +1,367 @@ +#include +#include + +#include "crypto_eddsa.h" +#include "crypto_helpers.h" + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // ULONG, LPCWSTR + +#include // BCrypt*, BCRYPT_* + +#include // std::countl_zero +#include // std::size_t +#include // std::uint8_t +#include // std::memcpy +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { + +// The parsed key keeps both the algorithm provider and the imported key handle +// alive for reuse. The Edwards curves have no CNG primitive, so they keep the +// raw encoded point and verify through the reference implementation +struct PublicKey::Internal { + PublicKey::Type kind; + BCRYPT_ALG_HANDLE algorithm; + BCRYPT_KEY_HANDLE key; + std::size_t field_bytes; + std::string modulus; + std::string edwards_point; + EdwardsCurve edwards_curve; +}; + +} // namespace sourcemeta::core + +namespace { + +auto to_cng_algorithm( + const sourcemeta::core::SignatureHashFunction hash) noexcept -> LPCWSTR { + switch (hash) { + case sourcemeta::core::SignatureHashFunction::SHA256: + return BCRYPT_SHA256_ALGORITHM; + case sourcemeta::core::SignatureHashFunction::SHA384: + return BCRYPT_SHA384_ALGORITHM; + case sourcemeta::core::SignatureHashFunction::SHA512: + return BCRYPT_SHA512_ALGORITHM; + } + + std::unreachable(); +} + +auto to_ecdsa_algorithm(const sourcemeta::core::EllipticCurve curve) noexcept + -> LPCWSTR { + switch (curve) { + case sourcemeta::core::EllipticCurve::P256: + return BCRYPT_ECDSA_P256_ALGORITHM; + case sourcemeta::core::EllipticCurve::P384: + return BCRYPT_ECDSA_P384_ALGORITHM; + case sourcemeta::core::EllipticCurve::P521: + return BCRYPT_ECDSA_P521_ALGORITHM; + } + + std::unreachable(); +} + +auto to_ecc_public_magic(const sourcemeta::core::EllipticCurve curve) noexcept + -> ULONG { + switch (curve) { + case sourcemeta::core::EllipticCurve::P256: + return BCRYPT_ECDSA_PUBLIC_P256_MAGIC; + case sourcemeta::core::EllipticCurve::P384: + return BCRYPT_ECDSA_PUBLIC_P384_MAGIC; + case sourcemeta::core::EllipticCurve::P521: + return BCRYPT_ECDSA_PUBLIC_P521_MAGIC; + } + + std::unreachable(); +} + +struct KeyPair { + BCRYPT_ALG_HANDLE algorithm; + BCRYPT_KEY_HANDLE key; +}; + +auto native_rsa_key(const std::string_view modulus, + const std::string_view exponent) -> KeyPair { + const auto modulus_bit_length{ + (modulus.size() * 8u) - static_cast(std::countl_zero( + static_cast(modulus.front())))}; + + BCRYPT_RSAKEY_BLOB header{}; + header.Magic = BCRYPT_RSAPUBLIC_MAGIC; + header.BitLength = static_cast(modulus_bit_length); + header.cbPublicExp = static_cast(exponent.size()); + header.cbModulus = static_cast(modulus.size()); + header.cbPrime1 = 0; + header.cbPrime2 = 0; + + std::string blob; + blob.resize(sizeof(header)); + std::memcpy(blob.data(), &header, sizeof(header)); + blob.append(exponent); + blob.append(modulus); + + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, BCRYPT_RSA_ALGORITHM, nullptr, 0))) { + return {.algorithm = nullptr, .key = nullptr}; + } + + BCRYPT_KEY_HANDLE key{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptImportKeyPair(algorithm, nullptr, BCRYPT_RSAPUBLIC_BLOB, &key, + reinterpret_cast(blob.data()), + static_cast(blob.size()), 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + return {.algorithm = nullptr, .key = nullptr}; + } + + return {.algorithm = algorithm, .key = key}; +} + +auto native_ec_key(const sourcemeta::core::EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y, const std::size_t width) + -> KeyPair { + BCRYPT_ECCKEY_BLOB header{}; + header.dwMagic = to_ecc_public_magic(curve); + header.cbKey = static_cast(width); + + std::string blob; + blob.resize(sizeof(header)); + std::memcpy(blob.data(), &header, sizeof(header)); + blob.append(sourcemeta::core::pad_left(coordinate_x, width, '\x00')); + blob.append(sourcemeta::core::pad_left(coordinate_y, width, '\x00')); + + BCRYPT_ALG_HANDLE algorithm{nullptr}; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider( + &algorithm, to_ecdsa_algorithm(curve), nullptr, 0))) { + return {.algorithm = nullptr, .key = nullptr}; + } + + BCRYPT_KEY_HANDLE key{nullptr}; + if (!BCRYPT_SUCCESS( + BCryptImportKeyPair(algorithm, nullptr, BCRYPT_ECCPUBLIC_BLOB, &key, + reinterpret_cast(blob.data()), + static_cast(blob.size()), 0))) { + BCryptCloseAlgorithmProvider(algorithm, 0); + return {.algorithm = nullptr, .key = nullptr}; + } + + return {.algorithm = algorithm, .key = key}; +} + +auto verify_rsa(BCRYPT_KEY_HANDLE key, + const sourcemeta::core::SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature, const bool probabilistic) + -> bool { + const auto digest{sourcemeta::core::digest_message(hash, message)}; + + if (probabilistic) { + // The digest-length salt is what RFC 7518 Section 3.5 requires + BCRYPT_PSS_PADDING_INFO padding{}; + padding.pszAlgId = to_cng_algorithm(hash); + padding.cbSalt = static_cast(digest.size()); + return BCRYPT_SUCCESS(BCryptVerifySignature( + key, &padding, + reinterpret_cast(const_cast(digest.data())), + static_cast(digest.size()), + reinterpret_cast(const_cast(signature.data())), + static_cast(signature.size()), BCRYPT_PAD_PSS)); + } + + BCRYPT_PKCS1_PADDING_INFO padding{}; + padding.pszAlgId = to_cng_algorithm(hash); + return BCRYPT_SUCCESS(BCryptVerifySignature( + key, &padding, + reinterpret_cast(const_cast(digest.data())), + static_cast(digest.size()), + reinterpret_cast(const_cast(signature.data())), + static_cast(signature.size()), BCRYPT_PAD_PKCS1)); +} + +} // namespace + +namespace sourcemeta::core { + +PublicKey::PublicKey(Internal *internal) noexcept : internal_{internal} {} + +PublicKey::~PublicKey() { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + BCryptDestroyKey(internal_->key); + } + + if (internal_->algorithm != nullptr) { + BCryptCloseAlgorithmProvider(internal_->algorithm, 0); + } + + delete internal_; + } +} + +PublicKey::PublicKey(PublicKey &&other) noexcept : internal_{other.internal_} { + other.internal_ = nullptr; +} + +auto PublicKey::operator=(PublicKey &&other) noexcept -> PublicKey & { + if (this != &other) { + if (internal_ != nullptr) { + if (internal_->key != nullptr) { + BCryptDestroyKey(internal_->key); + } + + if (internal_->algorithm != nullptr) { + BCryptCloseAlgorithmProvider(internal_->algorithm, 0); + } + + delete internal_; + } + + internal_ = other.internal_; + other.internal_ = nullptr; + } + + return *this; +} + +auto PublicKey::type() const noexcept -> Type { return internal_->kind; } + +auto make_rsa_public_key(const std::string_view modulus, + const std::string_view exponent) + -> std::optional { + auto stripped_modulus{std::string{strip_left(modulus, '\x00')}}; + const auto stripped_exponent{strip_left(exponent, '\x00')}; + if (stripped_modulus.empty() || stripped_exponent.empty() || + stripped_modulus.size() > MAXIMUM_KEY_BYTES || + stripped_exponent.size() > MAXIMUM_KEY_BYTES) { + return std::nullopt; + } + + const auto pair{native_rsa_key(stripped_modulus, stripped_exponent)}; + if (pair.key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::RSA, + .algorithm = pair.algorithm, + .key = pair.key, + .field_bytes = 0, + .modulus = std::move(stripped_modulus), + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_ec_public_key(const EllipticCurve curve, + const std::string_view coordinate_x, + const std::string_view coordinate_y) + -> std::optional { + const auto width{curve_field_bytes(curve)}; + const auto stripped_x{strip_left(coordinate_x, '\x00')}; + const auto stripped_y{strip_left(coordinate_y, '\x00')}; + if (stripped_x.size() > width || stripped_y.size() > width) { + return std::nullopt; + } + + const auto pair{native_ec_key(curve, stripped_x, stripped_y, width)}; + if (pair.key == nullptr) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::EllipticCurve, + .algorithm = pair.algorithm, + .key = pair.key, + .field_bytes = width, + .modulus = {}, + .edwards_point = {}, + .edwards_curve = {}}}; +} + +auto make_eddsa_public_key(const EdwardsCurve curve, + const std::string_view public_key) + -> std::optional { + if (public_key.size() != eddsa_public_key_bytes(curve)) { + return std::nullopt; + } + + return PublicKey{ + new PublicKey::Internal{.kind = PublicKey::Type::Edwards, + .algorithm = nullptr, + .key = nullptr, + .field_bytes = 0, + .modulus = {}, + .edwards_point = std::string{public_key}, + .edwards_curve = curve}}; +} + +auto rsassa_pkcs1_v15_verify(const PublicKey &key, + const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, false); +} + +auto rsassa_pss_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA || + !rsa_signature_in_range(signature, internal->modulus)) { + return false; + } + + return verify_rsa(internal->key, hash, message, signature, true); +} + +auto ecdsa_verify(const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::EllipticCurve || + signature.size() != internal->field_bytes * 2) { + return false; + } + + const auto digest{digest_message(hash, message)}; + + // The CNG signature format is the raw fixed-width R || S concatenation, so + // the input passes through unchanged + return BCRYPT_SUCCESS(BCryptVerifySignature( + internal->key, nullptr, + reinterpret_cast(const_cast(digest.data())), + static_cast(digest.size()), + reinterpret_cast(const_cast(signature.data())), + static_cast(signature.size()), 0)); +} + +auto eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool { + const auto *internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::Edwards) { + return false; + } + + switch (internal->edwards_curve) { + case EdwardsCurve::Ed25519: + return edwards25519_verify(internal->edwards_point, message, signature); + case EdwardsCurve::Ed448: + return edwards448_verify(internal->edwards_point, message, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/crypto/include/sourcemeta/core/crypto.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..38cc7ae3 --- /dev/null +++ b/vendor/core/src/core/crypto/include/sourcemeta/core/crypto_verify.h @@ -0,0 +1,174 @@ +#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::optional +#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 +/// The Edwards curves supported by signature verification. +enum class EdwardsCurve : std::uint8_t { Ed25519, Ed448 }; + +/// @ingroup crypto +/// A parsed public key that holds the native key, so that the same key can +/// verify many signatures without paying the key construction cost on every +/// call. Build it once with one of the factory functions and pass it to the +/// matching verification function. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_rsa_public_key(modulus, exponent)}; +/// assert(key.has_value()); +/// assert(sourcemeta::core::rsassa_pkcs1_v15_verify( +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, message, +/// signature)); +/// ``` +class SOURCEMETA_CORE_CRYPTO_EXPORT PublicKey { +public: + /// The kind of key, which fixes the signature schemes it can verify. + enum class Type : std::uint8_t { RSA, EllipticCurve, Edwards }; + + ~PublicKey(); + PublicKey(PublicKey &&other) noexcept; + auto operator=(PublicKey &&other) noexcept -> PublicKey &; + PublicKey(const PublicKey &) = delete; + auto operator=(const PublicKey &) -> PublicKey & = delete; + + /// The kind of key this is. + [[nodiscard]] auto type() const noexcept -> Type; + + /// The backend specific parsed key state, defined by each backend + struct Internal; + /// Take ownership of a parsed key. Prefer the factory functions below + explicit PublicKey(Internal *internal) noexcept; + /// Access the parsed key, which the verification functions read. The type is + /// opaque, so there is nothing a caller can do with it + [[nodiscard]] auto internal() const noexcept -> const Internal * { + return this->internal_; + } + +private: + Internal *internal_; +}; + +/// @ingroup crypto +/// Parse an RSA public key from its raw big-endian modulus and exponent bytes, +/// returning no value when the material is malformed or beyond 4096 bits. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_rsa_public_key( + const std::string_view modulus, const std::string_view exponent) + -> std::optional; + +/// @ingroup crypto +/// Parse an elliptic curve public key from its raw big-endian point +/// coordinates, returning no value when the point is malformed. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_ec_public_key( + const EllipticCurve curve, const std::string_view coordinate_x, + const std::string_view coordinate_y) -> std::optional; + +/// @ingroup crypto +/// Parse an Edwards-curve public key from its raw encoded point, returning no +/// value when the key is malformed or the wrong length for the curve. +auto SOURCEMETA_CORE_CRYPTO_EXPORT make_eddsa_public_key( + const EdwardsCurve curve, const std::string_view public_key) + -> std::optional; + +/// @ingroup crypto +/// Verify an RSASSA-PKCS1-v1_5 signature (RFC 8017 Section 8.2.2) over a +/// message with the given RSA key. The signature is invalid rather than an +/// error if it is malformed or the key is not an RSA key. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_rsa_public_key(modulus, exponent)}; +/// assert(key.has_value()); +/// assert(!sourcemeta::core::rsassa_pkcs1_v15_verify( +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, "message", +/// "signature")); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT rsassa_pkcs1_v15_verify( + const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, const std::string_view signature) -> bool; + +/// @ingroup crypto +/// Verify an RSASSA-PSS signature (RFC 8017 Section 8.1.2) over a message with +/// the given RSA key. The salt is expected to be as long as the hash function +/// output, as RFC 7518 requires, and signatures carrying any other salt length +/// are invalid, as are signatures verified against a non-RSA key. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_rsa_public_key(modulus, exponent)}; +/// assert(key.has_value()); +/// assert(!sourcemeta::core::rsassa_pss_verify( +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, "message", +/// "signature")); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT rsassa_pss_verify( + const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, const std::string_view signature) -> bool; + +/// @ingroup crypto +/// Verify an ECDSA signature (FIPS 186-4 Section 6.4) over a message with the +/// given elliptic curve key. The signature is the raw concatenation of the two +/// integers, each padded to the curve field width, as JWS mandates (RFC 7518 +/// Section 3.4). The signature is invalid rather than an error if it is +/// malformed or the key is not an elliptic curve key. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_ec_public_key( +/// sourcemeta::core::EllipticCurve::P256, x, y)}; +/// assert(key.has_value()); +/// assert(!sourcemeta::core::ecdsa_verify( +/// key.value(), sourcemeta::core::SignatureHashFunction::SHA256, "message", +/// "signature")); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT ecdsa_verify( + const PublicKey &key, const SignatureHashFunction hash, + const std::string_view message, const std::string_view signature) -> bool; + +/// @ingroup crypto +/// Verify an EdDSA signature (RFC 8032) over a message with the given Edwards +/// curve key. There is no separate hash function, as the curve fixes it. The +/// signature is invalid rather than an error if it is malformed or the key is +/// not an Edwards curve key. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::make_eddsa_public_key( +/// sourcemeta::core::EdwardsCurve::Ed25519, public_key)}; +/// assert(key.has_value()); +/// assert(!sourcemeta::core::eddsa_verify(key.value(), "message", +/// "signature")); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT +eddsa_verify(const PublicKey &key, const std::string_view message, + const std::string_view signature) -> bool; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/http/CMakeLists.txt b/vendor/core/src/core/http/CMakeLists.txt index 60034a76..e81a3b9c 100644 --- a/vendor/core/src/core/http/CMakeLists.txt +++ b/vendor/core/src/core/http/CMakeLists.txt @@ -1,12 +1,38 @@ +if(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_curl.cc) +elseif(APPLE) + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_darwin.mm) +elseif(WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "MSYS") + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_windows.cc) +else() + set(SOURCEMETA_CORE_HTTP_CLIENT_SOURCE client_curl.cc) +endif() + sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME http - PRIVATE_HEADERS problem.h status.h + PRIVATE_HEADERS problem.h status.h method.h message.h error.h system.h SOURCES helpers.h problem.cc match_accept.cc match_accept_language.cc negotiate_encoding.cc from_date.cc format_link.cc field_list.cc - accept_includes_all.cc content_type_matches.cc) + accept_includes_all.cc content_type_matches.cc parse_bearer.cc + ${SOURCEMETA_CORE_HTTP_CLIENT_SOURCE}) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME http) endif() target_link_libraries(sourcemeta_core_http PUBLIC sourcemeta::core::json) +target_link_libraries(sourcemeta_core_http PUBLIC sourcemeta::core::text) target_link_libraries(sourcemeta_core_http PRIVATE sourcemeta::core::time) + +if(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) + find_package(CURL REQUIRED) + target_compile_definitions(sourcemeta_core_http + PRIVATE SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) + target_link_libraries(sourcemeta_core_http PRIVATE CURL::libcurl) +elseif(APPLE) + target_link_libraries(sourcemeta_core_http PRIVATE "-framework Foundation") +elseif(WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "MSYS") + target_link_libraries(sourcemeta_core_http PRIVATE winhttp) + target_link_libraries(sourcemeta_core_http PRIVATE sourcemeta::core::unicode) +else() + target_link_libraries(sourcemeta_core_http PRIVATE ${CMAKE_DL_LIBS}) +endif() diff --git a/vendor/core/src/core/http/client_curl.cc b/vendor/core/src/core/http/client_curl.cc new file mode 100644 index 00000000..0b7c89d9 --- /dev/null +++ b/vendor/core/src/core/http/client_curl.cc @@ -0,0 +1,410 @@ +#include + +#ifdef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL +#include // curl_easy_*, curl_slist_*, curl_global_init, CURLOPT_* +#endif + +#include // std::size_t +#include // std::uint16_t +#include // std::optional +#include // std::string +#include // std::string_view + +#ifndef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL +#include // dlopen, dlsym, dlerror, RTLD_NOW + +#include // std::array +#include // std::getenv +#include // std::memcpy +#include // std::vector + +// When cURL is not linked at build time, we load it at runtime via dlopen +// and therefore need no curl headers. The following reproduces the small +// subset of libcurl's C API this backend uses. Every type, prototype, and +// option id is part of libcurl's frozen ABI (SONAME libcurl.so.4, stable +// since 2006), so these values never change. The prototypes are never +// called directly (we invoke them through dlsym'd pointers) nor linked; +// they exist only so the shared CurlApi table can derive their types +extern "C" { + +using CURL = void; +using CURLcode = int; +using CURLoption = int; +using CURLINFO = int; +using curl_off_t = long long; + +struct curl_slist; + +auto curl_global_init(long flags) -> CURLcode; +auto curl_easy_init() -> CURL *; +auto curl_easy_cleanup(CURL *handle) -> void; +auto curl_easy_setopt(CURL *handle, CURLoption option, ...) -> CURLcode; +auto curl_easy_perform(CURL *handle) -> CURLcode; +auto curl_easy_getinfo(CURL *handle, CURLINFO info, ...) -> CURLcode; +auto curl_easy_strerror(CURLcode code) -> const char *; +auto curl_slist_append(curl_slist *list, const char *value) -> curl_slist *; +auto curl_slist_free_all(curl_slist *list) -> void; + +} // extern "C" + +// Option ids are a type-class base plus an index in libcurl's headers; the +// resolved values are reproduced here (see the trailing comments) +constexpr CURLcode CURLE_OK{0}; +constexpr long CURL_GLOBAL_ALL{3}; // SSL(1<<0) | WIN32(1<<1) +constexpr CURLoption CURLOPT_URL{10002}; // STRINGPOINT + 2 +constexpr CURLoption CURLOPT_FOLLOWLOCATION{52}; // LONG + 52 +constexpr CURLoption CURLOPT_MAXREDIRS{68}; // LONG + 68 +constexpr CURLoption CURLOPT_NOSIGNAL{99}; // LONG + 99 +constexpr CURLoption CURLOPT_ACCEPT_ENCODING{10102}; // STRINGPOINT + 102 +constexpr CURLoption CURLOPT_TIMEOUT_MS{155}; // LONG + 155 +constexpr CURLoption CURLOPT_CONNECTTIMEOUT_MS{156}; // LONG + 156 +constexpr CURLoption CURLOPT_WRITEFUNCTION{20011}; // FUNCTIONPOINT + 11 +constexpr CURLoption CURLOPT_WRITEDATA{10001}; // CBPOINT + 1 +constexpr CURLoption CURLOPT_HEADERFUNCTION{20079}; // FUNCTIONPOINT + 79 +constexpr CURLoption CURLOPT_HEADERDATA{10029}; // CBPOINT + 29 +constexpr CURLoption CURLOPT_POSTFIELDSIZE_LARGE{30120}; // OFF_T + 120 +constexpr CURLoption CURLOPT_POSTFIELDS{10015}; // OBJECTPOINT + 15 +constexpr CURLoption CURLOPT_HTTPHEADER{10023}; // SLISTPOINT + 23 +constexpr CURLoption CURLOPT_NOBODY{44}; // LONG + 44 +constexpr CURLoption CURLOPT_CUSTOMREQUEST{10036}; // STRINGPOINT + 36 +constexpr CURLINFO CURLINFO_RESPONSE_CODE{2097154}; // CURLINFO_LONG(0x200000)+2 +constexpr CURLINFO CURLINFO_EFFECTIVE_URL{ + 1048577}; // CURLINFO_STRING(0x100000)+1 +#endif + +namespace { + +constexpr std::string_view HTTP_RESPONSE_TOO_LARGE_MESSAGE{ + "The response exceeds the maximum allowed size"}; + +// The subset of the libcurl C API this backend relies on, captured as +// function pointers so the request logic is shared between the link-time +// backend (SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) and the default +// runtime-loaded (dlopen) backend. Member types are derived from the curl +// headers, so they stay in sync with the real prototypes +struct CurlApi { + decltype(&curl_global_init) global_init; + decltype(&curl_easy_init) easy_init; + decltype(&curl_easy_cleanup) easy_cleanup; + decltype(&curl_easy_setopt) easy_setopt; + decltype(&curl_easy_perform) easy_perform; + decltype(&curl_easy_getinfo) easy_getinfo; + decltype(&curl_easy_strerror) easy_strerror; + decltype(&curl_slist_append) slist_append; + decltype(&curl_slist_free_all) slist_free_all; +}; + +class CurlHandle { +public: + explicit CurlHandle(const CurlApi &api) + : api_{api}, handle_{api.easy_init()} {} + ~CurlHandle() { + if (this->handle_) { + this->api_.easy_cleanup(this->handle_); + } + } + + CurlHandle(const CurlHandle &) = delete; + auto operator=(const CurlHandle &) -> CurlHandle & = delete; + CurlHandle(CurlHandle &&) = delete; + auto operator=(CurlHandle &&) -> CurlHandle & = delete; + + [[nodiscard]] auto get() const -> CURL * { return this->handle_; } + explicit operator bool() const { return this->handle_ != nullptr; } + +private: + const CurlApi &api_; + CURL *handle_; +}; + +class CurlHeaderList { +public: + explicit CurlHeaderList(const CurlApi &api) : api_{api} {} + ~CurlHeaderList() { + if (this->list_) { + this->api_.slist_free_all(this->list_); + } + } + + CurlHeaderList(const CurlHeaderList &) = delete; + auto operator=(const CurlHeaderList &) -> CurlHeaderList & = delete; + CurlHeaderList(CurlHeaderList &&) = delete; + auto operator=(CurlHeaderList &&) -> CurlHeaderList & = delete; + + auto append(const std::string &line) -> void { + auto *result{this->api_.slist_append(this->list_, line.c_str())}; + if (result) { + this->list_ = result; + } + } + + [[nodiscard]] auto get() const -> curl_slist * { return this->list_; } + +private: + const CurlApi &api_; + curl_slist *list_{nullptr}; +}; + +struct BodyContext { + std::string *output; + std::optional maximum_size; + bool maximum_size_exceeded{false}; +}; + +auto body_callback(char *data, std::size_t size, std::size_t count, + void *user_data) -> std::size_t { + auto *context{static_cast(user_data)}; + const std::size_t chunk{size * count}; + if (context->maximum_size.has_value() && + (context->output->size() > context->maximum_size.value() || + chunk > context->maximum_size.value() - context->output->size())) { + context->maximum_size_exceeded = true; + // Returning a smaller count than given aborts the transfer + return 0; + } + + context->output->append(data, chunk); + return chunk; +} + +auto header_callback(char *data, std::size_t size, std::size_t count, + void *output) -> std::size_t { + sourcemeta::core::http_accumulate_header_line( + *static_cast(output), + std::string_view{data, size * count}); + return size * count; +} + +#ifndef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL + +using sourcemeta::core::HTTPSystemBackendError; + +constexpr std::string_view CURL_LIBRARY_ENV{"SOURCEMETA_CORE_CURL_SO"}; + +// Tried in order. Every entry carries the `.so.4` SONAME so we only ever +// bind an ABI-compatible cURL (never the unversioned `libcurl.so` dev +// symlink, which could point at a different major version). The bare +// soname is first because it resolves through the dynamic linker (ld.so +// cache on glibc, default /lib:/usr/lib on musl) and is present on every +// mainstream distribution. The absolute entries are fallbacks for +// environments where the cache is absent (custom prefixes, ldconfig not +// run). The trailing GnuTLS entry is an ABI-compatible last resort for +// minimal Debian and Ubuntu systems that ship only that build +constexpr std::array CURL_CANDIDATE_PATHS{ + {"libcurl.so.4", "/usr/lib/x86_64-linux-gnu/libcurl.so.4", + "/usr/lib/aarch64-linux-gnu/libcurl.so.4", + "/usr/lib/arm-linux-gnueabihf/libcurl.so.4", + "/usr/lib/i386-linux-gnu/libcurl.so.4", "/usr/lib64/libcurl.so.4", + "/lib64/libcurl.so.4", "/usr/lib/libcurl.so.4", + "/usr/local/lib/libcurl.so.4", "libcurl-gnutls.so.4"}}; + +struct ResolvedLibrary { + void *handle; + std::string path; +}; + +template +auto resolve_symbol(const ResolvedLibrary &library, const char *name) + -> Signature { + // Clear any stale error, then distinguish a null symbol from a null value + dlerror(); + void *symbol{dlsym(library.handle, name)}; + if (dlerror() != nullptr) { + throw HTTPSystemBackendError{ + "The cURL library was loaded but does not provide the expected API", + std::string{CURL_LIBRARY_ENV}, + {library.path}}; + } + + // Copy the pointer representation instead of reinterpret_cast, which the + // standard only conditionally supports for object-to-function conversions. + // POSIX guarantees dlsym results are convertible to function pointers + Signature function{}; + std::memcpy(&function, &symbol, sizeof(function)); + return function; +} + +auto open_library() -> ResolvedLibrary { + if (const auto *configured_path{std::getenv(CURL_LIBRARY_ENV.data())}; + configured_path != nullptr && configured_path[0] != '\0') { + if (auto *handle{dlopen(configured_path, RTLD_NOW)}; handle != nullptr) { + return {handle, configured_path}; + } + + throw HTTPSystemBackendError{ + "Could not load the cURL library from the configured path", + std::string{CURL_LIBRARY_ENV}, + {std::string{configured_path}}}; + } + + for (const auto candidate : CURL_CANDIDATE_PATHS) { + if (auto *handle{dlopen(candidate.data(), RTLD_NOW)}; handle != nullptr) { + return {handle, std::string{candidate}}; + } + } + + std::vector searched; + searched.reserve(CURL_CANDIDATE_PATHS.size()); + for (const auto candidate : CURL_CANDIDATE_PATHS) { + searched.emplace_back(candidate); + } + + throw HTTPSystemBackendError{ + "Could not find the system cURL library (libcurl)", + std::string{CURL_LIBRARY_ENV}, std::move(searched)}; +} + +auto load_curl() -> const CurlApi & { + // The handle is intentionally never dlclose()d: the function pointers + // must remain valid for the lifetime of the process + static const ResolvedLibrary library{open_library()}; + static const CurlApi api{ + .global_init = resolve_symbol( + library, "curl_global_init"), + .easy_init = resolve_symbol( + library, "curl_easy_init"), + .easy_cleanup = resolve_symbol( + library, "curl_easy_cleanup"), + .easy_setopt = resolve_symbol( + library, "curl_easy_setopt"), + .easy_perform = resolve_symbol( + library, "curl_easy_perform"), + .easy_getinfo = resolve_symbol( + library, "curl_easy_getinfo"), + .easy_strerror = resolve_symbol( + library, "curl_easy_strerror"), + .slist_append = resolve_symbol( + library, "curl_slist_append"), + .slist_free_all = resolve_symbol( + library, "curl_slist_free_all")}; + return api; +} + +#endif + +auto acquire_api() -> const CurlApi & { +#ifdef SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL + static const CurlApi api{.global_init = &curl_global_init, + .easy_init = &curl_easy_init, + .easy_cleanup = &curl_easy_cleanup, + .easy_setopt = &curl_easy_setopt, + .easy_perform = &curl_easy_perform, + .easy_getinfo = &curl_easy_getinfo, + .easy_strerror = &curl_easy_strerror, + .slist_append = &curl_slist_append, + .slist_free_all = &curl_slist_free_all}; + return api; +#else + return load_curl(); +#endif +} + +} // namespace + +namespace sourcemeta::core { + +auto HTTPSystemRequest::send() const -> HTTPResponse { + const CurlApi &api{acquire_api()}; + + static const CURLcode global_initialization{api.global_init(CURL_GLOBAL_ALL)}; + if (global_initialization != CURLE_OK) { + throw HTTPError{this->method_, this->url_, + api.easy_strerror(global_initialization)}; + } + + const CurlHandle handle{api}; + if (!handle) { + throw HTTPError{this->method_, this->url_, + "Failed to initialise the HTTP client"}; + } + + HTTPResponse response; + api.easy_setopt(handle.get(), CURLOPT_URL, this->url_.c_str()); + api.easy_setopt(handle.get(), CURLOPT_FOLLOWLOCATION, + this->follow_redirects_ ? 1L : 0L); + if (this->follow_redirects_) { + api.easy_setopt(handle.get(), CURLOPT_MAXREDIRS, + static_cast(this->maximum_redirects_)); + } + + api.easy_setopt(handle.get(), CURLOPT_NOSIGNAL, 1L); + api.easy_setopt(handle.get(), CURLOPT_TIMEOUT_MS, + static_cast(this->timeout_.count())); + if (this->connect_timeout_.has_value()) { + api.easy_setopt(handle.get(), CURLOPT_CONNECTTIMEOUT_MS, + static_cast(this->connect_timeout_.value().count())); + } + + // Advertise and transparently decode all supported content encodings, + // matching what the NSURLSession and WinHTTP backends do + api.easy_setopt(handle.get(), CURLOPT_ACCEPT_ENCODING, ""); + + std::string raw_headers; + BodyContext body_context{.output = &response.body, + .maximum_size = this->maximum_response_size_}; + api.easy_setopt(handle.get(), CURLOPT_WRITEFUNCTION, body_callback); + api.easy_setopt(handle.get(), CURLOPT_WRITEDATA, &body_context); + api.easy_setopt(handle.get(), CURLOPT_HEADERFUNCTION, header_callback); + api.easy_setopt(handle.get(), CURLOPT_HEADERDATA, &raw_headers); + + CurlHeaderList header_list{api}; + for (const auto &[name, value] : this->headers_) { + std::string line{name}; + // The semicolon form is how cURL distinguishes a header with an + // empty value from a header to suppress + if (value.empty()) { + line += ";"; + } else { + line += ": "; + line += value; + } + + header_list.append(line); + } + + if (this->body_.has_value()) { + std::string content_type_line{"Content-Type: "}; + content_type_line += this->body_.value().content_type; + header_list.append(content_type_line); + api.easy_setopt(handle.get(), CURLOPT_POSTFIELDSIZE_LARGE, + static_cast(this->body_.value().data.size())); + api.easy_setopt(handle.get(), CURLOPT_POSTFIELDS, + this->body_.value().data.data()); + } + + if (header_list.get()) { + api.easy_setopt(handle.get(), CURLOPT_HTTPHEADER, header_list.get()); + } + + const std::string method{http_method_string(this->method_)}; + if (this->method_ == HTTPMethod::HEAD) { + api.easy_setopt(handle.get(), CURLOPT_NOBODY, 1L); + } else if (this->method_ != HTTPMethod::GET || this->body_.has_value()) { + api.easy_setopt(handle.get(), CURLOPT_CUSTOMREQUEST, method.c_str()); + } + + const auto code{api.easy_perform(handle.get())}; + if (code != CURLE_OK) { + if (body_context.maximum_size_exceeded) { + throw HTTPError{this->method_, this->url_, + std::string{HTTP_RESPONSE_TOO_LARGE_MESSAGE}}; + } + + throw HTTPError{this->method_, this->url_, api.easy_strerror(code)}; + } + + long status_code{0}; + api.easy_getinfo(handle.get(), CURLINFO_RESPONSE_CODE, &status_code); + char *effective_url{nullptr}; + api.easy_getinfo(handle.get(), CURLINFO_EFFECTIVE_URL, &effective_url); + if (effective_url != nullptr) { + response.url.assign(effective_url); + } + + http_parse_headers(raw_headers, response.headers); + response.status = + http_status_from_code(static_cast(status_code)); + return response; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/client_darwin.mm b/vendor/core/src/core/http/client_darwin.mm new file mode 100644 index 00000000..84d4b731 --- /dev/null +++ b/vendor/core/src/core/http/client_darwin.mm @@ -0,0 +1,194 @@ +#include +#include + +// NSURL, NSMutableURLRequest, NSURLSession, NSHTTPURLResponse, dispatch_* +#import + +#include // std::size_t +#include // std::uint16_t +#include // std::string +#include // std::string_view +#include // std::move + +namespace { + +constexpr std::string_view HTTP_RESPONSE_TOO_LARGE_MESSAGE{ + "The response exceeds the maximum allowed size"}; + +auto to_nsstring(const std::string_view input) -> NSString * { + return [[NSString alloc] initWithBytes:input.data() + length:input.size() + encoding:NSUTF8StringEncoding]; +} + +} // namespace + +// The delegate-based API streams the response body in chunks, allowing +// the maximum response size to be enforced without first buffering the +// entire response in memory +@interface SourcemetaCoreHTTPDelegate : NSObject +@property(nonatomic, assign) sourcemeta::core::HTTPResponse *response; +@property(nonatomic, assign) std::string *failure; +@property(nonatomic, strong) dispatch_semaphore_t semaphore; +@property(nonatomic, assign) BOOL hasMaximumResponseSize; +@property(nonatomic, assign) std::size_t maximumResponseSize; +@property(nonatomic, assign) BOOL followRedirects; +@property(nonatomic, assign) std::size_t maximumRedirects; +@property(nonatomic, assign) std::size_t redirectCount; +@end + +@implementation SourcemetaCoreHTTPDelegate + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + willPerformHTTPRedirection:(NSHTTPURLResponse *)response + newRequest:(NSURLRequest *)request + completionHandler: + (void (^)(NSURLRequest *))completionHandler { + // Passing a nil request stops the redirection and delivers the redirect + // response itself as the final response + if (!self.followRedirects) { + completionHandler(nil); + return; + } + + self.redirectCount += 1; + if (self.redirectCount > self.maximumRedirects) { + self.failure->assign("The maximum number of redirects was exceeded"); + [task cancel]; + completionHandler(nil); + return; + } + + completionHandler(request); +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data { + auto *body{&self.response->body}; + if (self.hasMaximumResponseSize && + (body->size() > self.maximumResponseSize || + static_cast(data.length) > + self.maximumResponseSize - body->size())) { + self.failure->assign(HTTP_RESPONSE_TOO_LARGE_MESSAGE); + [dataTask cancel]; + return; + } + + [data enumerateByteRangesUsingBlock:^(const void *bytes, NSRange range, + BOOL *) { + body->append(static_cast(bytes), range.length); + }]; +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didCompleteWithError:(NSError *)error { + // A failure recorded while streaming, such as exceeding the maximum + // response size, takes precedence over the resulting cancellation error + if (self.failure->empty()) { + if (error != nil) { + self.failure->assign([error.localizedDescription UTF8String]); + } else if (![task.response isKindOfClass:[NSHTTPURLResponse class]]) { + self.failure->assign("The response is not an HTTP response"); + } else { + const auto *http_response{(NSHTTPURLResponse *)task.response}; + self.response->status = sourcemeta::core::http_status_from_code( + static_cast(http_response.statusCode)); + if (http_response.URL != nil) { + self.response->url.assign([http_response.URL.absoluteString UTF8String]); + } + + auto *headers{&self.response->headers}; + [http_response.allHeaderFields + enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSString *value, + BOOL *) { + std::string header_name{[name UTF8String]}; + sourcemeta::core::to_lowercase(header_name); + headers->emplace_back(std::move(header_name), [value UTF8String]); + }]; + } + } + + dispatch_semaphore_signal(self.semaphore); +} + +@end + +namespace sourcemeta::core { + +auto HTTPSystemRequest::send() const -> HTTPResponse { + HTTPResponse response; + // The delegate runs on a background queue, where throwing would + // terminate the process, so failures are recorded here and thrown + // from the calling thread once the request settles + std::string failure; + + @autoreleasepool { + NSURL *target{[NSURL URLWithString:to_nsstring(this->url_)]}; + if (target == nil) { + failure = "Invalid URL"; + } else { + NSMutableURLRequest *url_request{ + [NSMutableURLRequest requestWithURL:target]}; + url_request.HTTPMethod = to_nsstring(http_method_string(this->method_)); + for (const auto &[name, value] : this->headers_) { + // Repeated headers are folded into a single comma-separated field + // line, which is semantically equivalent per RFC 9110 + [url_request addValue:to_nsstring(value) + forHTTPHeaderField:to_nsstring(name)]; + } + + if (this->body_.has_value()) { + [url_request setValue:to_nsstring(this->body_.value().content_type) + forHTTPHeaderField:@"Content-Type"]; + url_request.HTTPBody = + [NSData dataWithBytes:this->body_.value().data.data() + length:this->body_.value().data.size()]; + } + + NSURLSessionConfiguration *configuration{ + [NSURLSessionConfiguration ephemeralSessionConfiguration]}; + configuration.timeoutIntervalForResource = + static_cast(this->timeout_.count()) / 1000.0; + if (this->connect_timeout_.has_value()) { + configuration.timeoutIntervalForRequest = + static_cast(this->connect_timeout_.value().count()) / + 1000.0; + } + + // The delegate completes before the semaphore is signalled, so + // pointing to the stack-allocated locals from it is safe + SourcemetaCoreHTTPDelegate *delegate{ + [[SourcemetaCoreHTTPDelegate alloc] init]}; + delegate.response = &response; + delegate.failure = &failure; + delegate.semaphore = dispatch_semaphore_create(0); + delegate.hasMaximumResponseSize = + this->maximum_response_size_.has_value() ? YES : NO; + delegate.maximumResponseSize = + this->maximum_response_size_.value_or(0); + delegate.followRedirects = this->follow_redirects_ ? YES : NO; + delegate.maximumRedirects = this->maximum_redirects_; + delegate.redirectCount = 0; + + NSURLSession *session{ + [NSURLSession sessionWithConfiguration:configuration + delegate:delegate + delegateQueue:nil]}; + NSURLSessionDataTask *task{[session dataTaskWithRequest:url_request]}; + [task resume]; + dispatch_semaphore_wait(delegate.semaphore, DISPATCH_TIME_FOREVER); + [session finishTasksAndInvalidate]; + } + } + + if (!failure.empty()) { + throw HTTPError{this->method_, this->url_, failure}; + } + + return response; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/client_windows.cc b/vendor/core/src/core/http/client_windows.cc new file mode 100644 index 00000000..609115db --- /dev/null +++ b/vendor/core/src/core/http/client_windows.cc @@ -0,0 +1,277 @@ +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#include // DWORD, GetLastError, LPVOID +#include // WinHttp* + +// `windows.h` defines a `DELETE` macro that conflicts with +// `sourcemeta::core::HTTPMethod::DELETE` +#ifdef DELETE +#undef DELETE +#endif + +#include + +#include // std::chrono::milliseconds +#include // std::size_t +#include // std::uint16_t +#include // std::numeric_limits +#include // std::string, std::wstring +#include // std::wstring_view +#include // std::pair +#include // std::vector + +namespace { + +constexpr std::string_view HTTP_RESPONSE_TOO_LARGE_MESSAGE{ + "The response exceeds the maximum allowed size"}; + +// WinHttpSetTimeouts takes signed millisecond counts where zero requests no +// timeout. Floor non-positive durations to the smallest bound so a misused +// value cannot become an unbounded wait, and saturate large ones to avoid a +// narrowing wrap +auto to_winhttp_timeout(const std::chrono::milliseconds value) -> int { + if (value.count() <= 0) { + return 1; + } + + if (value.count() > std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + + return static_cast(value.count()); +} + +class WinHTTPHandle { +public: + WinHTTPHandle(const HINTERNET handle) : handle_{handle} {} + ~WinHTTPHandle() { + if (this->handle_) { + WinHttpCloseHandle(this->handle_); + } + } + + WinHTTPHandle(const WinHTTPHandle &) = delete; + auto operator=(const WinHTTPHandle &) -> WinHTTPHandle & = delete; + WinHTTPHandle(WinHTTPHandle &&) = delete; + auto operator=(WinHTTPHandle &&) -> WinHTTPHandle & = delete; + + auto get() const -> HINTERNET { return this->handle_; } + explicit operator bool() const { return this->handle_ != nullptr; } + +private: + HINTERNET handle_; +}; + +auto parse_response_headers( + const HINTERNET request, + std::vector> &headers) -> void { + DWORD size{0}; + WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, WINHTTP_NO_OUTPUT_BUFFER, + &size, WINHTTP_NO_HEADER_INDEX); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + return; + } + + std::wstring buffer(size / sizeof(wchar_t), L'\0'); + if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, buffer.data(), &size, + WINHTTP_NO_HEADER_INDEX)) { + return; + } + + sourcemeta::core::http_parse_headers(sourcemeta::core::wide_to_utf8(buffer), + headers); +} + +auto query_effective_url(const HINTERNET request) -> std::string { + DWORD size{0}; + WinHttpQueryOption(request, WINHTTP_OPTION_URL, nullptr, &size); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || size == 0) { + return {}; + } + + std::wstring buffer(size / sizeof(wchar_t), L'\0'); + if (!WinHttpQueryOption(request, WINHTTP_OPTION_URL, buffer.data(), &size)) { + return {}; + } + + buffer.resize(size / sizeof(wchar_t)); + if (!buffer.empty() && buffer.back() == L'\0') { + buffer.pop_back(); + } + + return sourcemeta::core::wide_to_utf8(buffer); +} + +} // namespace + +namespace sourcemeta::core { + +auto HTTPSystemRequest::send() const -> HTTPResponse { + HTTPResponse response; + + const auto wide_url{sourcemeta::core::utf8_to_wide(this->url_)}; + URL_COMPONENTS components{}; + components.dwStructSize = sizeof(components); + components.dwHostNameLength = static_cast(-1); + components.dwUrlPathLength = static_cast(-1); + components.dwExtraInfoLength = static_cast(-1); + if (!WinHttpCrackUrl(wide_url.c_str(), 0, 0, &components)) { + throw HTTPError{this->method_, this->url_, "Invalid URL"}; + } + + const std::wstring host{components.lpszHostName, components.dwHostNameLength}; + std::wstring path{components.lpszUrlPath, components.dwUrlPathLength}; + if (components.lpszExtraInfo) { + // The fragment, if any, must never be sent to the server + const std::wstring_view extra_information{components.lpszExtraInfo, + components.dwExtraInfoLength}; + path.append(extra_information.substr(0, extra_information.find(L'#'))); + } + + const WinHTTPHandle session{ + WinHttpOpen(nullptr, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0)}; + if (!session) { + throw HTTPError{this->method_, this->url_, + "Failed to initialise the HTTP client"}; + } + + const WinHTTPHandle connection{ + WinHttpConnect(session.get(), host.c_str(), components.nPort, 0)}; + if (!connection) { + throw HTTPError{this->method_, this->url_, "Failed to connect to the host"}; + } + + const auto secure{components.nScheme == INTERNET_SCHEME_HTTPS}; + const auto method{ + sourcemeta::core::utf8_to_wide(http_method_string(this->method_))}; + const WinHTTPHandle request_handle{WinHttpOpenRequest( + connection.get(), method.c_str(), path.c_str(), nullptr, + WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, + secure ? WINHTTP_FLAG_SECURE : 0)}; + if (!request_handle) { + throw HTTPError{this->method_, this->url_, + "Failed to create the HTTP request"}; + } + + if (this->follow_redirects_) { + DWORD maximum_redirects{static_cast(this->maximum_redirects_)}; + WinHttpSetOption(request_handle.get(), + WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS, + &maximum_redirects, sizeof(maximum_redirects)); + } else { + DWORD policy{WINHTTP_OPTION_REDIRECT_POLICY_NEVER}; + WinHttpSetOption(request_handle.get(), WINHTTP_OPTION_REDIRECT_POLICY, + &policy, sizeof(policy)); + } + + // The total timeout bounds sending the request and receiving the response, + // and also caps the resolution and connection phases unless a narrower + // connect timeout is given, so it acts as an overall ceiling + const auto total_timeout{to_winhttp_timeout(this->timeout_)}; + const auto connect_timeout{ + this->connect_timeout_.has_value() + ? to_winhttp_timeout(this->connect_timeout_.value()) + : total_timeout}; + WinHttpSetTimeouts(request_handle.get(), connect_timeout, connect_timeout, + total_timeout, total_timeout); + + DWORD decompression{WINHTTP_DECOMPRESSION_FLAG_ALL}; + WinHttpSetOption(request_handle.get(), WINHTTP_OPTION_DECOMPRESSION, + &decompression, sizeof(decompression)); + + auto serialized_headers{http_serialize_headers(this->headers_)}; + LPVOID body_data{WINHTTP_NO_REQUEST_DATA}; + DWORD body_size{0}; + if (this->body_.has_value()) { + if (this->body_.value().data.size() > std::numeric_limits::max()) { + throw HTTPError{this->method_, this->url_, + "The request body is too large"}; + } + + serialized_headers += "Content-Type: "; + serialized_headers += this->body_.value().content_type; + serialized_headers += "\r\n"; + body_data = const_cast(this->body_.value().data.data()); + body_size = static_cast(this->body_.value().data.size()); + } + + const auto request_headers{ + sourcemeta::core::utf8_to_wide(serialized_headers)}; + + if (!WinHttpSendRequest( + request_handle.get(), + request_headers.empty() ? WINHTTP_NO_ADDITIONAL_HEADERS + : request_headers.c_str(), + request_headers.empty() ? 0 + : static_cast(request_headers.size()), + body_data, body_size, body_size, 0)) { + throw HTTPError{this->method_, this->url_, + "Failed to send the HTTP request"}; + } + + if (!WinHttpReceiveResponse(request_handle.get(), nullptr)) { + throw HTTPError{this->method_, this->url_, + "Failed to receive the HTTP response"}; + } + + DWORD status_code{0}; + DWORD status_code_size{sizeof(status_code)}; + if (!WinHttpQueryHeaders(request_handle.get(), + WINHTTP_QUERY_STATUS_CODE | + WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, &status_code, + &status_code_size, WINHTTP_NO_HEADER_INDEX)) { + throw HTTPError{this->method_, this->url_, + "Failed to read the HTTP response status"}; + } + + parse_response_headers(request_handle.get(), response.headers); + response.url = query_effective_url(request_handle.get()); + + while (true) { + DWORD available{0}; + if (!WinHttpQueryDataAvailable(request_handle.get(), &available)) { + throw HTTPError{this->method_, this->url_, + "Failed to read the HTTP response body"}; + } + + if (available == 0) { + break; + } + + if (this->maximum_response_size_.has_value() && + (response.body.size() > this->maximum_response_size_.value() || + available > + this->maximum_response_size_.value() - response.body.size())) { + throw HTTPError{this->method_, this->url_, + std::string{HTTP_RESPONSE_TOO_LARGE_MESSAGE}}; + } + + const auto offset{response.body.size()}; + response.body.resize(offset + available); + DWORD read{0}; + if (!WinHttpReadData(request_handle.get(), response.body.data() + offset, + available, &read)) { + throw HTTPError{this->method_, this->url_, + "Failed to read the HTTP response body"}; + } + + response.body.resize(offset + read); + } + + response.status = + http_status_from_code(static_cast(status_code)); + return response; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http.h b/vendor/core/src/core/http/include/sourcemeta/core/http.h index deb42874..f572a6e3 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http.h @@ -6,8 +6,12 @@ #endif // NOLINTBEGIN(misc-include-cleaner) +#include +#include +#include #include #include +#include // NOLINTEND(misc-include-cleaner) #include // std::chrono::system_clock @@ -247,6 +251,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/include/sourcemeta/core/http_error.h b/vendor/core/src/core/http/include/sourcemeta/core/http_error.h new file mode 100644 index 00000000..45975ee0 --- /dev/null +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_error.h @@ -0,0 +1,98 @@ +#ifndef SOURCEMETA_CORE_HTTP_ERROR_H_ +#define SOURCEMETA_CORE_HTTP_ERROR_H_ + +#ifndef SOURCEMETA_CORE_HTTP_EXPORT +#include +#endif + +#include +#include + +#include // std::uint16_t +#include // std::runtime_error +#include // std::string +#include // std::move + +namespace sourcemeta::core { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup http +/// An error that prevented obtaining a response, such as a connection +/// failure, a name resolution failure, or a TLS failure. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const sourcemeta::core::HTTPError error{ +/// sourcemeta::core::HTTPMethod::GET, +/// "https://example.com", "Connection refused"}; +/// assert(error.method() == sourcemeta::core::HTTPMethod::GET); +/// assert(error.url() == "https://example.com"); +/// ``` +class SOURCEMETA_CORE_HTTP_EXPORT HTTPError : public std::runtime_error { +public: + HTTPError(const HTTPMethod method, std::string url, + const std::string &message) + : std::runtime_error{message}, method_{method}, url_{std::move(url)} {} + + /// Get the request method that triggered the failure + [[nodiscard]] auto method() const noexcept -> HTTPMethod { + return this->method_; + } + + /// Get the request URL that triggered the failure + [[nodiscard]] auto url() const noexcept -> const std::string & { + return this->url_; + } + +private: + HTTPMethod method_; + std::string url_; +}; + +/// @ingroup http +/// An error for a response with an unsuccessful status code, owning a copy +/// of the status data. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const sourcemeta::core::HTTPStatusError error{ +/// sourcemeta::core::HTTPMethod::GET, +/// "https://example.com", sourcemeta::core::HTTP_STATUS_NOT_FOUND}; +/// assert(error.status() == sourcemeta::core::HTTP_STATUS_NOT_FOUND); +/// ``` +class SOURCEMETA_CORE_HTTP_EXPORT HTTPStatusError : public HTTPError { +public: + HTTPStatusError(const HTTPMethod method, std::string url, + const HTTPStatus &status) + : HTTPError{method, std::move(url), "Unsuccessful HTTP response"}, + code_{status.code}, phrase_{status.phrase}, wire_{status.wire} {} + + /// Get the response status that triggered the failure. The contained + /// views borrow from this error and stay valid for its lifetime + [[nodiscard]] auto status() const noexcept -> HTTPStatus { + return {.code = this->code_, .phrase = this->phrase_, .wire = this->wire_}; + } + +private: + std::uint16_t code_; + std::string phrase_; + std::string wire_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_message.h b/vendor/core/src/core/http/include/sourcemeta/core/http_message.h new file mode 100644 index 00000000..29f8763a --- /dev/null +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_message.h @@ -0,0 +1,280 @@ +#ifndef SOURCEMETA_CORE_HTTP_MESSAGE_H_ +#define SOURCEMETA_CORE_HTTP_MESSAGE_H_ + +#include + +#include // std::convertible_to, std::invocable +#include // std::size_t +#include // std::nullopt, std::optional +#include // std::string +#include // std::string_view +#include // std::move + +namespace sourcemeta::core { + +/// @ingroup http +/// Test whether a raw line opens a message header block per RFC 9112 §4. +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_is_status_line("HTTP/1.1 200 OK")); +/// assert(!sourcemeta::core::http_is_status_line("Content-Type: text/html")); +/// ``` +inline constexpr auto http_is_status_line(const std::string_view line) noexcept + -> bool { + // The prefix cannot open a field line, as RFC 9110 §5.6.2 defines tchar + // as "any VCHAR, except delimiters" where delimiters include the slash, + // and the protocol name is case-sensitive per RFC 9112 §2.3 + return line.starts_with("HTTP/"); +} + +/// @ingroup http +/// Accumulate raw header lines into a buffer, retaining only the block of +/// the most recent message, given that transparently following redirects or +/// receiving interim responses produces one header block per message. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// std::string buffer; +/// sourcemeta::core::http_accumulate_header_line(buffer, +/// "HTTP/1.1 301 Moved Permanently\r\n"); +/// sourcemeta::core::http_accumulate_header_line(buffer, +/// "HTTP/1.1 200 OK\r\n"); +/// assert(buffer == "HTTP/1.1 200 OK\r\n"); +/// ``` +template + requires requires(Buffer buffer, std::string_view line) { + buffer.clear(); + buffer.append(line); + } +inline auto http_accumulate_header_line(Buffer &buffer, + const std::string_view line) -> void { + if (http_is_status_line(line)) { + buffer.clear(); + } + + buffer.append(line); +} + +/// @ingroup http +/// Parse the field lines of a raw message header block per RFC 9112 §5, +/// skipping the start line and invoking the callback with each raw field +/// name and its value with optional whitespace excluded. A continuation of +/// the previous value through deprecated line folding is reported with an +/// empty name. Malformed field lines are discarded. Neither argument +/// allocates, as both are views into the input. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// sourcemeta::core::http_parse_headers( +/// "HTTP/1.1 200 OK\r\nServer: test\r\n\r\n", +/// [](const std::string_view name, const std::string_view value) { +/// assert(name == "Server"); +/// assert(value == "test"); +/// }); +/// ``` +template + requires std::invocable +inline auto http_parse_headers(const std::string_view input, Callback callback) + -> void { + std::size_t cursor{input.find("\r\n")}; + while (cursor != std::string_view::npos) { + cursor += 2; + const auto end{input.find("\r\n", cursor)}; + if (end == std::string_view::npos || end == cursor) { + break; + } + + auto line{input.substr(cursor, end - cursor)}; + cursor = end; + + // RFC 9112 §5.2 deprecates obs-fold, defined as "OWS CRLF RWS", and + // mandates that a user agent "MUST replace each received obs-fold + // with one or more SP octets prior to interpreting the field value", + // so a line opening with whitespace continues the previous field + // line value and is reported with an empty name for the caller to join + if (line.front() == ' ' || line.front() == '\t') { + while (!line.empty() && (line.front() == ' ' || line.front() == '\t')) { + line.remove_prefix(1); + } + + while (!line.empty() && (line.back() == ' ' || line.back() == '\t')) { + line.remove_suffix(1); + } + + callback(std::string_view{}, line); + continue; + } + + const auto parts{split_once(line, ':')}; + if (!parts.has_value() || parts->first.empty()) { + continue; + } + + const auto name{parts->first}; + // RFC 9112 §5.1 mandates that "no whitespace is allowed between the + // field name and colon" because in the past such whitespace has "led + // to security vulnerabilities", so the field line is discarded + if (name.back() == ' ' || name.back() == '\t') { + continue; + } + + auto value{parts->second}; + // RFC 9112 §5 defines a field line as + // `field-name ":" OWS field-value OWS` where RFC 9110 §5.6.3 defines + // optional whitespace as `*( SP / HTAB )` + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { + value.remove_prefix(1); + } + + while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) { + value.remove_suffix(1); + } + + callback(name, value); + } +} + +/// @ingroup http +/// Parse the field lines of a raw message header block, skipping the start +/// line, into any container of name and value pairs, normalising names to +/// lowercase given that RFC 9110 §5.1 mandates that "field names are +/// case-insensitive", preserving repeated fields as separate entries, and +/// joining deprecated line folding per RFC 9112 §5.2. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// #include +/// +/// std::vector> headers; +/// sourcemeta::core::http_parse_headers( +/// "HTTP/1.1 200 OK\r\nServer: test\r\n\r\n", headers); +/// assert(headers.size() == 1); +/// assert(headers.at(0).first == "server"); +/// assert(headers.at(0).second == "test"); +/// ``` +template + requires requires(Container container, std::string name, std::string value, + const char character) { + container.emplace_back(std::move(name), std::move(value)); + { container.empty() } -> std::convertible_to; + container.back().second += character; + } +inline auto http_parse_headers(const std::string_view input, Container &headers) + -> void { + http_parse_headers(input, [&headers](const std::string_view name, + const std::string_view value) { + if (name.empty()) { + // RFC 9112 §5.2 mandates replacing "each received obs-fold with + // one or more SP octets prior to interpreting the field value" + if (!headers.empty()) { + auto &previous_value{headers.back().second}; + previous_value += ' '; + previous_value += value; + } + + return; + } + + std::string header_name{name}; + to_lowercase(header_name); + headers.emplace_back(std::move(header_name), std::string{value}); + }); +} + +/// @ingroup http +/// Serialise headers, given as any range of name and value pairs, into +/// CRLF-delimited field lines per RFC 9112 §5. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// #include +/// +/// const std::vector> headers{ +/// {"Accept", "application/json"}}; +/// assert(sourcemeta::core::http_serialize_headers(headers) == +/// "Accept: application/json\r\n"); +/// ``` +template +inline auto http_serialize_headers(const Headers &headers) -> std::string { + std::size_t total_size{0}; + for (const auto &[name, value] : headers) { + // Account for the colon, the space, and the trailing CRLF + total_size += name.size() + value.size() + 4; + } + + std::string result; + result.reserve(total_size); + for (const auto &[name, value] : headers) { + // RFC 9112 §5.1 notes that "a single SP preceding the field line + // value is preferred for consistent readability by humans" + result += name; + result += ": "; + result += value; + result += "\r\n"; + } + + return result; +} + +/// @ingroup http +/// Find the value of the first header with the given lowercase name in any +/// range of name and value pairs, returning no result when absent. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// #include +/// +/// const std::vector> headers{ +/// {"server", "test"}}; +/// assert(sourcemeta::core::http_header_find(headers, "server").has_value()); +/// assert(!sourcemeta::core::http_header_find(headers, "date").has_value()); +/// ``` +template +inline auto http_header_find(const Headers &headers, + const std::string_view name) + -> std::optional { + // Prefer the container's own lookup when it supports searching by the + // given name without converting it, as associative containers do it in + // logarithmic or constant time instead of a linear scan + if constexpr (requires { headers.find(name) != headers.end(); }) { + const auto match{headers.find(name)}; + if (match != headers.end()) { + return std::string_view{match->second}; + } + + return std::nullopt; + } else { + for (const auto &[header_name, header_value] : headers) { + if (header_name == name) { + return std::string_view{header_value}; + } + } + + return std::nullopt; + } +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_method.h b/vendor/core/src/core/http/include/sourcemeta/core/http_method.h new file mode 100644 index 00000000..767a1c11 --- /dev/null +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_method.h @@ -0,0 +1,63 @@ +#ifndef SOURCEMETA_CORE_HTTP_METHOD_H_ +#define SOURCEMETA_CORE_HTTP_METHOD_H_ + +#include // std::uint8_t +#include // std::string_view +#include // std::unreachable + +namespace sourcemeta::core { + +/// @ingroup http +/// A request method per RFC 9110 §9.3 and RFC 5789 §2. +enum class HTTPMethod : std::uint8_t { + GET, + HEAD, + POST, + PUT, + DELETE, + CONNECT, + OPTIONS, + TRACE, + PATCH +}; + +/// @ingroup http +/// Convert a request method into its case-sensitive token per RFC 9110 §9.1. +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_method_string( +/// sourcemeta::core::HTTPMethod::GET) == "GET"); +/// ``` +inline constexpr auto http_method_string(const HTTPMethod method) noexcept + -> std::string_view { + switch (method) { + case HTTPMethod::GET: + return "GET"; + case HTTPMethod::HEAD: + return "HEAD"; + case HTTPMethod::POST: + return "POST"; + case HTTPMethod::PUT: + return "PUT"; + case HTTPMethod::DELETE: + return "DELETE"; + case HTTPMethod::CONNECT: + return "CONNECT"; + case HTTPMethod::OPTIONS: + return "OPTIONS"; + case HTTPMethod::TRACE: + return "TRACE"; + case HTTPMethod::PATCH: + return "PATCH"; + } + + std::unreachable(); +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_status.h b/vendor/core/src/core/http/include/sourcemeta/core/http_status.h index 3a82995c..c95c6235 100644 --- a/vendor/core/src/core/http/include/sourcemeta/core/http_status.h +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_status.h @@ -10,7 +10,7 @@ namespace sourcemeta::core { /// A typed HTTP status code per RFC 9110 §15. For example: /// /// ```cpp -/// #include +/// #include /// #include /// /// assert(sourcemeta::core::HTTP_STATUS_OK.code == 200); @@ -386,6 +386,150 @@ inline constexpr HTTPStatus HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED{ .phrase = "Network Authentication Required", .wire = "511 Network Authentication Required"}; +/// @ingroup http +/// Resolve a numeric status code into its registered status, with unknown +/// codes resolving to an empty reason phrase. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::http_status_from_code(200) == +/// sourcemeta::core::HTTP_STATUS_OK); +/// assert(sourcemeta::core::http_status_from_code(599).phrase.empty()); +/// ``` +inline constexpr auto http_status_from_code(const std::uint16_t code) noexcept + -> HTTPStatus { + switch (code) { + case 100: + return HTTP_STATUS_CONTINUE; + case 101: + return HTTP_STATUS_SWITCHING_PROTOCOLS; + case 102: + return HTTP_STATUS_PROCESSING; + case 103: + return HTTP_STATUS_EARLY_HINTS; + case 200: + return HTTP_STATUS_OK; + case 201: + return HTTP_STATUS_CREATED; + case 202: + return HTTP_STATUS_ACCEPTED; + case 203: + return HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION; + case 204: + return HTTP_STATUS_NO_CONTENT; + case 205: + return HTTP_STATUS_RESET_CONTENT; + case 206: + return HTTP_STATUS_PARTIAL_CONTENT; + case 207: + return HTTP_STATUS_MULTI_STATUS; + case 208: + return HTTP_STATUS_ALREADY_REPORTED; + case 226: + return HTTP_STATUS_IM_USED; + case 300: + return HTTP_STATUS_MULTIPLE_CHOICES; + case 301: + return HTTP_STATUS_MOVED_PERMANENTLY; + case 302: + return HTTP_STATUS_FOUND; + case 303: + return HTTP_STATUS_SEE_OTHER; + case 304: + return HTTP_STATUS_NOT_MODIFIED; + case 305: + return HTTP_STATUS_USE_PROXY; + case 307: + return HTTP_STATUS_TEMPORARY_REDIRECT; + case 308: + return HTTP_STATUS_PERMANENT_REDIRECT; + case 400: + return HTTP_STATUS_BAD_REQUEST; + case 401: + return HTTP_STATUS_UNAUTHORIZED; + case 402: + return HTTP_STATUS_PAYMENT_REQUIRED; + case 403: + return HTTP_STATUS_FORBIDDEN; + case 404: + return HTTP_STATUS_NOT_FOUND; + case 405: + return HTTP_STATUS_METHOD_NOT_ALLOWED; + case 406: + return HTTP_STATUS_NOT_ACCEPTABLE; + case 407: + return HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED; + case 408: + return HTTP_STATUS_REQUEST_TIMEOUT; + case 409: + return HTTP_STATUS_CONFLICT; + case 410: + return HTTP_STATUS_GONE; + case 411: + return HTTP_STATUS_LENGTH_REQUIRED; + case 412: + return HTTP_STATUS_PRECONDITION_FAILED; + case 413: + return HTTP_STATUS_CONTENT_TOO_LARGE; + case 414: + return HTTP_STATUS_URI_TOO_LONG; + case 415: + return HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE; + case 416: + return HTTP_STATUS_RANGE_NOT_SATISFIABLE; + case 417: + return HTTP_STATUS_EXPECTATION_FAILED; + case 418: + return HTTP_STATUS_IM_A_TEAPOT; + case 421: + return HTTP_STATUS_MISDIRECTED_REQUEST; + case 422: + return HTTP_STATUS_UNPROCESSABLE_CONTENT; + case 423: + return HTTP_STATUS_LOCKED; + case 424: + return HTTP_STATUS_FAILED_DEPENDENCY; + case 425: + return HTTP_STATUS_TOO_EARLY; + case 426: + return HTTP_STATUS_UPGRADE_REQUIRED; + case 428: + return HTTP_STATUS_PRECONDITION_REQUIRED; + case 429: + return HTTP_STATUS_TOO_MANY_REQUESTS; + case 431: + return HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE; + case 451: + return HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS; + case 500: + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + case 501: + return HTTP_STATUS_NOT_IMPLEMENTED; + case 502: + return HTTP_STATUS_BAD_GATEWAY; + case 503: + return HTTP_STATUS_SERVICE_UNAVAILABLE; + case 504: + return HTTP_STATUS_GATEWAY_TIMEOUT; + case 505: + return HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED; + case 506: + return HTTP_STATUS_VARIANT_ALSO_NEGOTIATES; + case 507: + return HTTP_STATUS_INSUFFICIENT_STORAGE; + case 508: + return HTTP_STATUS_LOOP_DETECTED; + case 510: + return HTTP_STATUS_NOT_EXTENDED; + case 511: + return HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED; + default: + return HTTPStatus{.code = code, .phrase = {}, .wire = {}}; + } +} + } // namespace sourcemeta::core #endif diff --git a/vendor/core/src/core/http/include/sourcemeta/core/http_system.h b/vendor/core/src/core/http/include/sourcemeta/core/http_system.h new file mode 100644 index 00000000..2d85dcbf --- /dev/null +++ b/vendor/core/src/core/http/include/sourcemeta/core/http_system.h @@ -0,0 +1,192 @@ +#ifndef SOURCEMETA_CORE_HTTP_SYSTEM_H_ +#define SOURCEMETA_CORE_HTTP_SYSTEM_H_ + +#ifndef SOURCEMETA_CORE_HTTP_EXPORT +#include +#endif + +#include +#include + +#include // std::chrono::milliseconds, std::chrono::seconds +#include // std::size_t +#include // std::optional +#include // std::runtime_error +#include // std::string +#include // std::move, std::pair +#include // std::vector + +namespace sourcemeta::core { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup http +/// The result of performing a request against a system HTTP backend. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// sourcemeta::core::HTTPSystemRequest request{"https://example.com"}; +/// const auto response{request.send()}; +/// assert(response.status == sourcemeta::core::HTTP_STATUS_OK); +/// ``` +struct HTTPResponse { + /// The response status code + HTTPStatus status{}; + /// The response headers, with names normalised to lowercase. Repeated + /// headers are preserved as separate entries, except on backends that fold + /// them into a single comma-separated entry, which is semantically + /// equivalent per RFC 9110 + std::vector> headers; + /// The response body, owned by this result + std::string body; + /// The effective URL after any followed redirects + std::string url; +}; + +/// @ingroup http +/// An error that prevented loading the underlying system HTTP backend, such +/// as a missing dynamically loaded library. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const sourcemeta::core::HTTPSystemBackendError error{ +/// "Could not find the system cURL library", "SOURCEMETA_CORE_CURL_SO", +/// {"libcurl.so.4"}}; +/// assert(error.variable() == "SOURCEMETA_CORE_CURL_SO"); +/// ``` +class SOURCEMETA_CORE_HTTP_EXPORT HTTPSystemBackendError + : public std::runtime_error { +public: + HTTPSystemBackendError(const std::string &message, std::string variable, + std::vector paths) + : std::runtime_error{message}, variable_{std::move(variable)}, + paths_{std::move(paths)} {} + + /// Get the name of the environment variable that overrides the backend path + [[nodiscard]] auto variable() const noexcept -> const std::string & { + return this->variable_; + } + + /// Get the paths that were searched while looking for the backend + [[nodiscard]] auto paths() const noexcept + -> const std::vector & { + return this->paths_; + } + +private: + std::string variable_; + std::vector paths_; +}; + +/// @ingroup http +/// A simple cross-platform HTTP request that delegates to the system HTTP +/// stack, NSURLSession on Apple platforms, WinHTTP on Windows, and cURL +/// everywhere else. The request owns its data, configure it with the builder +/// methods and perform it with `send`. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// sourcemeta::core::HTTPSystemRequest request{ +/// "https://example.com", sourcemeta::core::HTTPMethod::POST}; +/// request.header("Accept", "application/json"); +/// request.body("{}", "application/json"); +/// const auto response{request.send()}; +/// assert(response.status == sourcemeta::core::HTTP_STATUS_OK); +/// ``` +class SOURCEMETA_CORE_HTTP_EXPORT HTTPSystemRequest { +public: + explicit HTTPSystemRequest(std::string url, + const HTTPMethod method = HTTPMethod::GET) + : url_{std::move(url)}, method_{method} {} + + /// Set the request method + auto method(const HTTPMethod method) -> HTTPSystemRequest & { + this->method_ = method; + return *this; + } + + /// Add a request header. Repeated names are permitted + auto header(std::string name, std::string value) -> HTTPSystemRequest & { + this->headers_.emplace_back(std::move(name), std::move(value)); + return *this; + } + + /// Set the request body, sent along with the given `Content-Type` header + auto body(std::string data, std::string content_type) -> HTTPSystemRequest & { + this->body_ = + Body{.data = std::move(data), .content_type = std::move(content_type)}; + return *this; + } + + /// Set whether to follow redirects, on by default + auto follow_redirects(const bool value) -> HTTPSystemRequest & { + this->follow_redirects_ = value; + return *this; + } + + /// Set the maximum number of redirects to follow, 20 by default + auto maximum_redirects(const std::size_t value) -> HTTPSystemRequest & { + this->maximum_redirects_ = value; + return *this; + } + + /// Set the total request timeout, 30 seconds by default + auto timeout(const std::chrono::milliseconds value) -> HTTPSystemRequest & { + this->timeout_ = value; + return *this; + } + + /// Set a best-effort timeout for establishing the connection, applied as + /// each backend allows and falling back to the backend default when unset + auto connect_timeout(const std::chrono::milliseconds value) + -> HTTPSystemRequest & { + this->connect_timeout_ = value; + return *this; + } + + /// Abort with an error if the response body exceeds this number of bytes + auto maximum_response_size(const std::size_t value) -> HTTPSystemRequest & { + this->maximum_response_size_ = value; + return *this; + } + + /// Perform the request. A failure to obtain a response is reported as an + /// error, while unsuccessful status codes are returned on the result + [[nodiscard]] auto send() const -> HTTPResponse; + +private: + struct Body { + std::string data; + std::string content_type; + }; + + std::string url_; + HTTPMethod method_; + std::vector> headers_; + std::optional body_; + bool follow_redirects_{true}; + std::size_t maximum_redirects_{20}; + std::chrono::milliseconds timeout_{std::chrono::seconds{30}}; + std::optional connect_timeout_; + std::optional maximum_response_size_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/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..3923db25 --- /dev/null +++ b/vendor/core/src/core/jose/CMakeLists.txt @@ -0,0 +1,18 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jose + PRIVATE_HEADERS algorithm.h error.h jwk.h jwks.h jwt.h verify.h + SOURCES jose_algorithm.cc jose_jwk.cc jose_jwks.cc jose_jwt.cc + jose_jwt_check_claims.cc jose_jws_verify_signature.cc + jose_jwt_verify_signature.cc jose_jwt_verify.cc) + +target_link_libraries(sourcemeta_core_jose + PUBLIC sourcemeta::core::json) +target_link_libraries(sourcemeta_core_jose + PUBLIC sourcemeta::core::crypto) +target_link_libraries(sourcemeta_core_jose + PRIVATE sourcemeta::core::text) +target_link_libraries(sourcemeta_core_jose + PRIVATE sourcemeta::core::time) + +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME jose) +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..47193a2a --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose.h @@ -0,0 +1,26 @@ +#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 +#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..3f938c10 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_algorithm.h @@ -0,0 +1,49 @@ +#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 and +/// the Edwards-curve algorithm from RFC 8037 Section 3.1. The symmetric HMAC +/// family and the null algorithm are intentionally absent, which makes +/// algorithm confusion attacks unrepresentable in the type system. +enum class JWSAlgorithm : std::uint8_t { + RS256, + RS384, + RS512, + PS256, + PS384, + PS512, + ES256, + ES384, + ES512, + EdDSA +}; + +/// @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..9cd17fc5 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_error.h @@ -0,0 +1,49 @@ +#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"; + } +}; + +/// @ingroup jose +/// An error that occurs when parsing an invalid JSON Web Token. +class SOURCEMETA_CORE_JOSE_EXPORT JWTParseError : public std::exception { +public: + [[nodiscard]] auto what() const noexcept -> const char * override { + return "The input is not a valid JSON Web Token"; + } +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // 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..879873b9 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwk.h @@ -0,0 +1,111 @@ +#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 + +#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, elliptic curve, +/// and octet key pair (RFC 8037) keys. The key owns its decoded material, so +/// the source JSON document does not need to outlive it. For example: +/// +/// ```cpp +/// #include +/// #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, OctetKeyPair }; + + /// 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); + + /// A key exclusively owns its parsed public key, so it is move-only. + JWK(JWK &&other) noexcept = default; + auto operator=(JWK &&other) noexcept -> JWK & = default; + JWK(const JWK &) = delete; + auto operator=(const JWK &) -> JWK & = delete; + + /// Parse a JSON Web Key from a JSON value, returning no value on invalid + /// input. + [[nodiscard]] static auto from(const JSON &value) -> std::optional; + + /// 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_; + } + + // Elliptic curve keys (RFC 7518 Section 6.2) and octet key pairs (RFC 8037 + // Section 2) carry a curve name, which the elliptic curve algorithms pin to + // exactly one curve + [[nodiscard]] auto curve() const noexcept -> std::string_view { + return this->curve_; + } + + // The parsed platform key, built once from the decoded material so that + // verification reuses it rather than reconstructing it per signature. It is + // null when the material could not be turned into a key + [[nodiscard]] auto public_key() const noexcept -> const PublicKey * { + return this->public_key_.has_value() ? &*this->public_key_ : nullptr; + } + +private: + 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 curve_; + std::optional public_key_; +#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..9880b683 --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwks.h @@ -0,0 +1,89 @@ +#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); + + /// A key set exclusively owns its keys, so it is move-only. + JWKS(JWKS &&other) noexcept = default; + auto operator=(JWKS &&other) noexcept -> JWKS & = default; + JWKS(const JWKS &) = delete; + auto operator=(const JWKS &) -> JWKS & = delete; + + /// Parse a JSON Web Key Set from a JSON value, returning no value on invalid + /// input. + [[nodiscard]] static auto from(const JSON &value) -> std::optional; + [[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/include/sourcemeta/core/jose_jwt.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwt.h new file mode 100644 index 00000000..a4b51abc --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_jwt.h @@ -0,0 +1,118 @@ +#ifndef SOURCEMETA_CORE_JOSE_JWT_H_ +#define SOURCEMETA_CORE_JOSE_JWT_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include + +#include // std::chrono::system_clock::time_point +#include // std::optional +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup jose +/// A parsed JSON Web Token in compact serialization (RFC 7519, RFC 7515). The +/// token does not own its input, so the string it was parsed from must outlive +/// it. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY21lIn0.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// assert(token.value().algorithm() == sourcemeta::core::JWSAlgorithm::RS256); +/// ``` +class SOURCEMETA_CORE_JOSE_EXPORT JWT { +public: + /// Parse a JSON Web Token from its compact serialization, throwing a + /// `JWTParseError` on invalid input. + explicit JWT(const std::string_view input); + + /// Parse a JSON Web Token from its compact serialization, returning no value + /// on invalid input. + [[nodiscard]] static auto from(const std::string_view input) + -> std::optional; + + // Header (RFC 7515 Section 4) + + [[nodiscard]] auto algorithm() const noexcept -> std::optional { + return this->algorithm_; + } + + [[nodiscard]] auto key_id() const noexcept -> std::optional; + + [[nodiscard]] auto type() const noexcept -> std::optional; + + [[nodiscard]] auto header() const noexcept -> const JSON & { + return this->header_; + } + + // Registered claims (RFC 7519 Section 4.1) + + [[nodiscard]] auto issuer() const noexcept -> std::optional; + + [[nodiscard]] auto subject() const noexcept + -> std::optional; + + [[nodiscard]] auto + has_audience(const std::string_view audience) const noexcept -> bool; + + [[nodiscard]] auto expires_at() const + -> std::optional; + + [[nodiscard]] auto not_before() const + -> std::optional; + + [[nodiscard]] auto issued_at() const + -> std::optional; + + [[nodiscard]] auto token_id() const noexcept + -> std::optional; + + [[nodiscard]] auto payload() const noexcept -> const JSON & { + return this->payload_; + } + + // The exact wire bytes the signature is computed over, never re-serialized + // (RFC 7515 Section 5.1) + [[nodiscard]] auto signing_input() const noexcept -> std::string_view { + return this->signing_input_; + } + + [[nodiscard]] auto signature() const noexcept -> std::string_view { + return this->signature_; + } + +private: + JWT() = default; + static auto parse(const std::string_view input, JWT &result) -> bool; + +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + std::string_view signing_input_; + std::string signature_; + JSON header_{nullptr}; + JSON payload_{nullptr}; + std::optional algorithm_; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif +}; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h b/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h new file mode 100644 index 00000000..30b9d37d --- /dev/null +++ b/vendor/core/src/core/jose/include/sourcemeta/core/jose_verify.h @@ -0,0 +1,178 @@ +#ifndef SOURCEMETA_CORE_JOSE_VERIFY_H_ +#define SOURCEMETA_CORE_JOSE_VERIFY_H_ + +#ifndef SOURCEMETA_CORE_JOSE_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include // std::chrono::seconds, std::chrono::system_clock +#include // std::uint8_t +#include // std::optional +#include // std::span +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup jose +/// The claim validation errors that claim checking can return, one per check +/// performed rather than an exhaustive list of registered claims. +enum class JWTClaimError : std::uint8_t { + Issuer, + Subject, + Audience, + Expiration, + NotBefore, + IssuedAt +}; + +/// @ingroup jose +/// Validate the registered claims of a JSON Web Token against the expected +/// issuer and audience at a given time, returning the first failing check or no +/// value when every check passes. The expiration claim is required (RFC 9068 +/// Section 2.2), and the subject is checked only when an expected value is +/// supplied. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9." +/// "eyJpc3MiOiJhY21lIiwiYXVkIjoiY2xpZW50IiwiZXhwIjoyMDAwMDAwMDAwfQ.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// const auto error{sourcemeta::core::jwt_check_claims( +/// token.value(), "acme", "client", +/// std::chrono::system_clock::from_time_t(1500000000))}; +/// assert(!error.has_value()); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jwt_check_claims( + const JWT &token, const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew = std::chrono::seconds{0}, + const std::optional expected_subject = std::nullopt) + -> std::optional; + +/// @ingroup jose +/// Verify a JSON Web Signature given its algorithm, its signing input, and its +/// decoded signature against a JSON Web Key, returning false rather than +/// throwing for an unrecognized algorithm, a key whose type or curve cannot +/// serve the algorithm, a key declaring a contradicting algorithm, or a +/// signature that does not verify. The signing input is the exact bytes the +/// signature was computed over, which carry no constraint on their content. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::JWK::from(sourcemeta::core::parse_json( +/// R"JSON({ "kty": "RSA", "n": "", "e": "" })JSON"))}; +/// assert(!key.has_value() || +/// !sourcemeta::core::jws_verify_signature( +/// sourcemeta::core::JWSAlgorithm::RS256, "header.payload", +/// "signature", key.value())); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jws_verify_signature(const std::optional algorithm, + const std::string_view signing_input, + const std::string_view signature, const JWK &key) + -> bool; + +/// @ingroup jose +/// Verify the signature of a JSON Web Token against a JSON Web Key, returning +/// false rather than throwing whenever the token does not carry a confirmed +/// valid signature for the key. This includes an unrecognized algorithm, a key +/// whose type or curve cannot serve the algorithm, a key declaring a +/// contradicting algorithm, and a signature that does not verify. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY21lIn0.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// const auto key{sourcemeta::core::JWK::from( +/// sourcemeta::core::parse_json(R"JSON({ +/// "kty": "RSA", "n": "", "e": "" +/// })JSON"))}; +/// assert(!key.has_value() || +/// !sourcemeta::core::jwt_verify_signature(token.value(), key.value())); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jwt_verify_signature(const JWT &token, const JWK &key) -> bool; + +/// @ingroup jose +/// The steps of full token verification that can fail, in the order they are +/// evaluated. +enum class JWTVerificationError : std::uint8_t { + AlgorithmNotAllowed, + UnknownKey, + Signature, + Type, + Issuer, + Subject, + Audience, + Expiration, + NotBefore, + IssuedAt +}; + +/// @ingroup jose +/// Verify a JSON Web Token end to end against a key set, in the mandated order: +/// the algorithm must be in the allow-list, a key is selected by its identifier +/// or, when absent, tried against every compatible key, the signature must +/// verify, and the claims must pass. Returns no value when the token is fully +/// valid, or the first failing step. The type check enforces the access token +/// profile (RFC 9068 Section 2.1) only when an expected type is supplied. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// #include +/// +/// const std::string input{ +/// "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhY21lIn0.c2ln"}; +/// const auto token{sourcemeta::core::JWT::from(input)}; +/// assert(token.has_value()); +/// const auto keys{sourcemeta::core::JWKS::from( +/// sourcemeta::core::parse_json(R"JSON({ "keys": [] })JSON"))}; +/// assert(keys.has_value()); +/// const std::array allowed{sourcemeta::core::JWSAlgorithm::RS256}; +/// const auto error{sourcemeta::core::jwt_verify( +/// token.value(), keys.value(), allowed, "acme", "client", +/// std::chrono::system_clock::from_time_t(1500000000))}; +/// assert(error.has_value()); +/// ``` +SOURCEMETA_CORE_JOSE_EXPORT +auto jwt_verify( + const JWT &token, const JWKS &keys, + const std::span allowed_algorithms, + const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew = std::chrono::seconds{0}, + const std::optional expected_subject = std::nullopt, + const std::optional expected_type = std::nullopt) + -> std::optional; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jose/jose_algorithm.cc b/vendor/core/src/core/jose/jose_algorithm.cc new file mode 100644 index 00000000..185877eb --- /dev/null +++ b/vendor/core/src/core/jose/jose_algorithm.cc @@ -0,0 +1,35 @@ +#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 if (value == "EdDSA") { + return JWSAlgorithm::EdDSA; + } 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..5e249603 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwk.cc @@ -0,0 +1,271 @@ +#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, each ECDSA algorithm is tied to a +// specific curve (RFC 7518 Section 3.1), and the Edwards-curve algorithm +// requires an octet key pair of either curve (RFC 8037 Section 3.1) +auto algorithm_matches_key(const sourcemeta::core::JWSAlgorithm algorithm, + const sourcemeta::core::JWK::Type type, + const std::string_view curve) -> bool { + 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"; + case sourcemeta::core::JWSAlgorithm::EdDSA: + return type == sourcemeta::core::JWK::Type::OctetKeyPair; + } + + 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; + } +} + +// The public key octet length is fixed per Edwards curve (RFC 8032 Sections +// 5.1.5 and 5.2.5) +auto okp_key_bytes(const std::string_view curve) -> std::optional { + if (curve == "Ed25519") { + return 32; + } else if (curve == "Ed448") { + return 57; + } else { + return std::nullopt; + } +} + +// Both mappings are only reached after the curve has been validated above +auto to_elliptic_curve(const std::string_view curve) noexcept + -> sourcemeta::core::EllipticCurve { + if (curve == "P-256") { + return sourcemeta::core::EllipticCurve::P256; + } else if (curve == "P-384") { + return sourcemeta::core::EllipticCurve::P384; + } else { + return sourcemeta::core::EllipticCurve::P521; + } +} + +auto to_edwards_curve(const std::string_view curve) noexcept + -> sourcemeta::core::EdwardsCurve { + if (curve == "Ed25519") { + return sourcemeta::core::EdwardsCurve::Ed25519; + } else { + return sourcemeta::core::EdwardsCurve::Ed448; + } +} + +} // namespace + +namespace sourcemeta::core { + +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()}; + std::optional parsed_key; + if (key_type_value == "RSA") { + // A public key must not carry the private parameters (RFC 7518 Section + // 6.3.2), and rejecting them early surfaces dangerous misconfigurations + 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; + parsed_key = + make_rsa_public_key(decoded_modulus.value(), decoded_exponent.value()); + } else if (key_type_value == "EC") { + // A public key must not carry the private parameter (RFC 7518 Section + // 6.2.2) + 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(); + parsed_key = make_ec_public_key(to_elliptic_curve(result.curve_), + decoded_x.value(), decoded_y.value()); + } else if (key_type_value == "OKP") { + // A public key must not carry the private parameter (RFC 8037 Section 2) + if (value.try_at("d", HASH_D) != nullptr) { + return false; + } + + const auto *curve{value.try_at("crv", HASH_CRV)}; + const auto *public_key{value.try_at("x", HASH_X)}; + if (curve == nullptr || !curve->is_string() || public_key == nullptr || + !public_key->is_string()) { + return false; + } + + const auto key_bytes{okp_key_bytes(curve->to_string())}; + if (!key_bytes.has_value()) { + return false; + } + + auto decoded_public_key{base64url_decode(public_key->to_string())}; + if (!decoded_public_key.has_value() || + decoded_public_key.value().size() != key_bytes.value()) { + return false; + } + + result.type_ = Type::OctetKeyPair; + result.curve_ = curve->to_string(); + parsed_key = make_eddsa_public_key(to_edwards_curve(result.curve_), + decoded_public_key.value()); + } else { + return false; + } + + 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; + } + } + + // The platform key is built once when the material is decoded, so + // verification reuses it. A key that cannot be turned into one stays null and + // simply fails to verify + result.public_key_ = std::move(parsed_key); + return true; +} + +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/jose/jose_jws_verify_signature.cc b/vendor/core/src/core/jose/jose_jws_verify_signature.cc new file mode 100644 index 00000000..41555a61 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jws_verify_signature.cc @@ -0,0 +1,101 @@ +#include + +#include + +#include // std::optional +#include // std::string_view +#include // std::unreachable + +namespace { + +auto hash_for(const sourcemeta::core::JWSAlgorithm algorithm) + -> sourcemeta::core::SignatureHashFunction { + using sourcemeta::core::JWSAlgorithm; + using sourcemeta::core::SignatureHashFunction; + switch (algorithm) { + case JWSAlgorithm::RS256: + case JWSAlgorithm::PS256: + case JWSAlgorithm::ES256: + return SignatureHashFunction::SHA256; + case JWSAlgorithm::RS384: + case JWSAlgorithm::PS384: + case JWSAlgorithm::ES384: + return SignatureHashFunction::SHA384; + case JWSAlgorithm::RS512: + case JWSAlgorithm::PS512: + case JWSAlgorithm::ES512: + return SignatureHashFunction::SHA512; + // The Edwards-curve algorithm fixes its own hash, so it never reaches here + case JWSAlgorithm::EdDSA: + break; + } + + std::unreachable(); +} + +} // namespace + +namespace sourcemeta::core { + +auto jws_verify_signature(const std::optional algorithm, + const std::string_view signing_input, + const std::string_view signature, const JWK &key) + -> bool { + if (!algorithm.has_value()) { + return false; + } + + // A key that names an algorithm must not contradict the one in use (RFC 7517 + // Section 4.4) + if (key.algorithm().has_value() && + key.algorithm().value() != algorithm.value()) { + return false; + } + + // The key material is parsed into a reusable platform key when the key is + // constructed, so an absent one is material that never formed a valid key + const auto *public_key{key.public_key()}; + if (public_key == nullptr) { + return false; + } + + switch (algorithm.value()) { + case JWSAlgorithm::RS256: + case JWSAlgorithm::RS384: + case JWSAlgorithm::RS512: + return key.type() == JWK::Type::RSA && + rsassa_pkcs1_v15_verify(*public_key, hash_for(algorithm.value()), + signing_input, signature); + case JWSAlgorithm::PS256: + case JWSAlgorithm::PS384: + case JWSAlgorithm::PS512: + return key.type() == JWK::Type::RSA && + rsassa_pss_verify(*public_key, hash_for(algorithm.value()), + signing_input, signature); + // Each ECDSA algorithm is pinned to exactly one curve (RFC 7518 Section + // 3.4), so the key's curve is checked independently of any algorithm it + // declares + case JWSAlgorithm::ES256: + return key.type() == JWK::Type::EllipticCurve && key.curve() == "P-256" && + ecdsa_verify(*public_key, SignatureHashFunction::SHA256, + signing_input, signature); + case JWSAlgorithm::ES384: + return key.type() == JWK::Type::EllipticCurve && key.curve() == "P-384" && + ecdsa_verify(*public_key, SignatureHashFunction::SHA384, + signing_input, signature); + case JWSAlgorithm::ES512: + return key.type() == JWK::Type::EllipticCurve && key.curve() == "P-521" && + ecdsa_verify(*public_key, SignatureHashFunction::SHA512, + signing_input, signature); + // The Edwards-curve algorithm names one of two curves through the key + // rather than the algorithm (RFC 8037 Section 3.1), and the key fixes the + // curve when it is parsed + case JWSAlgorithm::EdDSA: + return key.type() == JWK::Type::OctetKeyPair && + eddsa_verify(*public_key, signing_input, signature); + } + + std::unreachable(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt.cc b/vendor/core/src/core/jose/jose_jwt.cc new file mode 100644 index 00000000..20bd6e1d --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt.cc @@ -0,0 +1,197 @@ +#include + +#include +#include +#include + +#include // std::chrono::duration, std::chrono::system_clock +#include // std::optional, std::nullopt +#include // std::out_of_range +#include // std::string_view +#include // std::move + +namespace { + +const auto HASH_ALG{sourcemeta::core::JSON::Object::hash("alg")}; +const auto HASH_CRIT{sourcemeta::core::JSON::Object::hash("crit")}; +const auto HASH_KID{sourcemeta::core::JSON::Object::hash("kid")}; +const auto HASH_TYP{sourcemeta::core::JSON::Object::hash("typ")}; +const auto HASH_ISS{sourcemeta::core::JSON::Object::hash("iss")}; +const auto HASH_SUB{sourcemeta::core::JSON::Object::hash("sub")}; +const auto HASH_AUD{sourcemeta::core::JSON::Object::hash("aud")}; +const auto HASH_EXP{sourcemeta::core::JSON::Object::hash("exp")}; +const auto HASH_NBF{sourcemeta::core::JSON::Object::hash("nbf")}; +const auto HASH_IAT{sourcemeta::core::JSON::Object::hash("iat")}; +const auto HASH_JTI{sourcemeta::core::JSON::Object::hash("jti")}; + +auto string_claim(const sourcemeta::core::JSON &object, + const sourcemeta::core::JSON::StringView name, + const sourcemeta::core::JSON::Object::hash_type hash) + -> std::optional { + const auto *member{object.try_at(name, hash)}; + if (member == nullptr || !member->is_string()) { + return std::nullopt; + } + + return std::string_view{member->to_string()}; +} + +auto date_claim(const sourcemeta::core::JSON &object, + const sourcemeta::core::JSON::StringView name, + const sourcemeta::core::JSON::Object::hash_type hash) + -> std::optional { + const auto *member{object.try_at(name, hash)}; + if (member == nullptr || !member->is_number()) { + return std::nullopt; + } + + // A NumericDate is the number of seconds since the Unix epoch, possibly + // non-integer (RFC 7519 Section 2). A decimal-backed number (such as the + // exponent form "1e9") whose magnitude exceeds the range of a double cannot + // stand for a usable timestamp, and untrusted input must not abort + double seconds{0}; + try { + seconds = member->as_real(); + } catch (const std::out_of_range &) { + return std::nullopt; + } + + return sourcemeta::core::from_unix_timestamp( + std::chrono::duration{seconds}); +} + +} // namespace + +namespace sourcemeta::core { + +auto JWT::parse(const std::string_view input, JWT &result) -> bool { + // The compact serialization is exactly three base64url segments joined by + // dots (RFC 7515 Section 7.1) + const auto first{split_once(input, '.')}; + if (!first.has_value()) { + return false; + } + + const auto second{split_once(first->second, '.')}; + if (!second.has_value()) { + return false; + } + + const auto header_segment{first->first}; + const auto payload_segment{second->first}; + const auto signature_segment{second->second}; + if (signature_segment.find('.') != std::string_view::npos) { + return false; + } + + auto header_bytes{base64url_decode(header_segment)}; + auto payload_bytes{base64url_decode(payload_segment)}; + auto signature_bytes{base64url_decode(signature_segment)}; + if (!header_bytes.has_value() || !payload_bytes.has_value() || + !signature_bytes.has_value()) { + return false; + } + + auto header_json{try_parse_json(header_bytes.value())}; + auto payload_json{try_parse_json(payload_bytes.value())}; + if (!header_json.has_value() || !header_json.value().is_object() || + !payload_json.has_value() || !payload_json.value().is_object()) { + return false; + } + + // The algorithm header parameter is required and must be a string (RFC 7515 + // Section 4.1.1) + const auto *algorithm{header_json.value().try_at("alg", HASH_ALG)}; + if (algorithm == nullptr || !algorithm->is_string()) { + return false; + } + + // Critical header extensions are not understood and must be rejected (RFC + // 7515 Section 4.1.11) + if (header_json.value().try_at("crit", HASH_CRIT) != nullptr) { + return false; + } + + result.algorithm_ = to_jws_algorithm(algorithm->to_string()); + result.signing_input_ = + input.substr(0, header_segment.size() + payload_segment.size() + 1); + result.signature_ = std::move(signature_bytes).value(); + result.header_ = std::move(header_json).value(); + result.payload_ = std::move(payload_json).value(); + return true; +} + +JWT::JWT(const std::string_view input) { + if (!parse(input, *this)) { + throw JWTParseError{}; + } +} + +auto JWT::from(const std::string_view input) -> std::optional { + JWT result; + if (parse(input, result)) { + return result; + } + + return std::nullopt; +} + +auto JWT::key_id() const noexcept -> std::optional { + return string_claim(this->header_, "kid", HASH_KID); +} + +auto JWT::type() const noexcept -> std::optional { + return string_claim(this->header_, "typ", HASH_TYP); +} + +auto JWT::issuer() const noexcept -> std::optional { + return string_claim(this->payload_, "iss", HASH_ISS); +} + +auto JWT::subject() const noexcept -> std::optional { + return string_claim(this->payload_, "sub", HASH_SUB); +} + +auto JWT::token_id() const noexcept -> std::optional { + return string_claim(this->payload_, "jti", HASH_JTI); +} + +auto JWT::has_audience(const std::string_view audience) const noexcept -> bool { + const auto *member{this->payload_.try_at("aud", HASH_AUD)}; + if (member == nullptr) { + return false; + } + + // The audience claim is either a single string or an array of strings (RFC + // 7519 Section 4.1.3) + if (member->is_string()) { + return member->to_string() == audience; + } + + if (member->is_array()) { + for (const auto &element : member->as_array()) { + if (element.is_string() && element.to_string() == audience) { + return true; + } + } + } + + return false; +} + +auto JWT::expires_at() const + -> std::optional { + return date_claim(this->payload_, "exp", HASH_EXP); +} + +auto JWT::not_before() const + -> std::optional { + return date_claim(this->payload_, "nbf", HASH_NBF); +} + +auto JWT::issued_at() const + -> std::optional { + return date_claim(this->payload_, "iat", HASH_IAT); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt_check_claims.cc b/vendor/core/src/core/jose/jose_jwt_check_claims.cc new file mode 100644 index 00000000..20ba883e --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt_check_claims.cc @@ -0,0 +1,69 @@ +#include + +#include // std::chrono::seconds, std::chrono::system_clock +#include // std::optional, std::nullopt +#include // std::string_view + +namespace sourcemeta::core { + +auto jwt_check_claims(const JWT &token, const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew, + const std::optional expected_subject) + -> std::optional { + // The issuer must be present and match the expected value (RFC 7519 Section + // 4.1.1) + const auto issuer{token.issuer()}; + if (!issuer.has_value() || issuer.value() != expected_issuer) { + return JWTClaimError::Issuer; + } + + // The subject is checked only when the caller pins one, since it is the + // authenticated principal that many flows accept as any valid identity (RFC + // 7519 Section 4.1.2) + if (expected_subject.has_value()) { + const auto subject{token.subject()}; + if (!subject.has_value() || subject.value() != expected_subject.value()) { + return JWTClaimError::Subject; + } + } + + // The audience must be present and contain the expected value (RFC 7519 + // Section 4.1.3) + if (!token.has_audience(expected_audience)) { + return JWTClaimError::Audience; + } + + // A bearer credential without an expiry is not acceptable for authentication, + // so the claim is required here even though RFC 7519 makes it optional in + // general (RFC 9068 Section 2.2) + const auto expires_at{token.expires_at()}; + if (!expires_at.has_value() || now >= expires_at.value() + clock_skew) { + return JWTClaimError::Expiration; + } + + // The not-before time, when present, must be a usable NumericDate that is not + // in the future. A claim that is present but malformed fails closed rather + // than being ignored (RFC 7519 Section 4.1.5) + const auto &payload{token.payload()}; + if (payload.defines("nbf")) { + const auto not_before{token.not_before()}; + if (!not_before.has_value() || now < not_before.value() - clock_skew) { + return JWTClaimError::NotBefore; + } + } + + // The issued-at time, when present, must be a usable NumericDate that is not + // in the future (RFC 7519 Section 4.1.6) + if (payload.defines("iat")) { + const auto issued_at{token.issued_at()}; + if (!issued_at.has_value() || now < issued_at.value() - clock_skew) { + return JWTClaimError::IssuedAt; + } + } + + return std::nullopt; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt_verify.cc b/vendor/core/src/core/jose/jose_jwt_verify.cc new file mode 100644 index 00000000..1590999b --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt_verify.cc @@ -0,0 +1,126 @@ +#include + +#include + +#include // std::ranges::find +#include // std::chrono::seconds, std::chrono::system_clock +#include // std::optional, std::nullopt +#include // std::span +#include // std::string_view +#include // std::unreachable + +namespace { + +// RFC 7519 Section 5.1: "a recipient using the media type value MUST treat it +// as if `application/` were prepended to any `typ` value not containing a `/`". +// Removing an explicit `application/` prefix, when nothing else in the value +// contains a slash, lets the compact `at+jwt` form and the full +// `application/at+jwt` form compare equal +auto strip_application_prefix(const std::string_view value) + -> std::string_view { + constexpr std::string_view prefix{"application/"}; + if (value.size() > prefix.size() && + sourcemeta::core::equals_ignore_case(value.substr(0, prefix.size()), + prefix) && + value.find('/', prefix.size()) == std::string_view::npos) { + return value.substr(prefix.size()); + } + + return value; +} + +auto to_verification_error(const sourcemeta::core::JWTClaimError error) + -> sourcemeta::core::JWTVerificationError { + using sourcemeta::core::JWTClaimError; + using sourcemeta::core::JWTVerificationError; + switch (error) { + case JWTClaimError::Issuer: + return JWTVerificationError::Issuer; + case JWTClaimError::Subject: + return JWTVerificationError::Subject; + case JWTClaimError::Audience: + return JWTVerificationError::Audience; + case JWTClaimError::Expiration: + return JWTVerificationError::Expiration; + case JWTClaimError::NotBefore: + return JWTVerificationError::NotBefore; + case JWTClaimError::IssuedAt: + return JWTVerificationError::IssuedAt; + } + + std::unreachable(); +} + +} // namespace + +namespace sourcemeta::core { + +auto jwt_verify(const JWT &token, const JWKS &keys, + const std::span allowed_algorithms, + const std::string_view expected_issuer, + const std::string_view expected_audience, + const std::chrono::system_clock::time_point now, + const std::chrono::seconds clock_skew, + const std::optional expected_subject, + const std::optional expected_type) + -> std::optional { + // The algorithm allow-list is enforced before any key is touched, per step 3 + // of the Sourcemeta One validation algorithm + const auto algorithm{token.algorithm()}; + if (!algorithm.has_value() || + std::ranges::find(allowed_algorithms, algorithm.value()) == + allowed_algorithms.end()) { + return JWTVerificationError::AlgorithmNotAllowed; + } + + // A token names its key through `kid` (RFC 7515 Section 4.1.4). When it does + // not, every key in the set is tried, since some providers omit it when they + // publish a single key. A missing or non-verifying key is reported as unknown + // rather than as a signature failure so that downstream can refetch the set, + // except when the named key is present but its signature does not verify + const auto key_id{token.key_id()}; + if (key_id.has_value()) { + const auto *key{keys.find(key_id.value())}; + if (key == nullptr) { + return JWTVerificationError::UnknownKey; + } + + if (!jwt_verify_signature(token, *key)) { + return JWTVerificationError::Signature; + } + } else { + bool verified{false}; + for (const auto &key : keys) { + if (jwt_verify_signature(token, key)) { + verified = true; + break; + } + } + + if (!verified) { + return JWTVerificationError::UnknownKey; + } + } + + // The type is a header concern checked only on an authenticated token, which + // is how the access token profile is enforced (RFC 9068 Section 2.1) + if (expected_type.has_value()) { + const auto type{token.type()}; + if (!type.has_value() || + !equals_ignore_case(strip_application_prefix(type.value()), + strip_application_prefix(expected_type.value()))) { + return JWTVerificationError::Type; + } + } + + const auto claim_error{jwt_check_claims(token, expected_issuer, + expected_audience, now, clock_skew, + expected_subject)}; + if (claim_error.has_value()) { + return to_verification_error(claim_error.value()); + } + + return std::nullopt; +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jose/jose_jwt_verify_signature.cc b/vendor/core/src/core/jose/jose_jwt_verify_signature.cc new file mode 100644 index 00000000..a0995879 --- /dev/null +++ b/vendor/core/src/core/jose/jose_jwt_verify_signature.cc @@ -0,0 +1,10 @@ +#include + +namespace sourcemeta::core { + +auto jwt_verify_signature(const JWT &token, const JWK &key) -> bool { + return jws_verify_signature(token.algorithm(), token.signing_input(), + token.signature(), key); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json.h b/vendor/core/src/core/json/include/sourcemeta/core/json.h index eec6bb84..f294776f 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json.h @@ -19,6 +19,7 @@ #include // std::basic_ifstream #include // std::initializer_list #include // std::basic_istream +#include // std::optional #include // std::basic_ostream #include // std::ostringstream #include // std::basic_string @@ -75,6 +76,25 @@ SOURCEMETA_CORE_JSON_EXPORT auto parse_json( const std::basic_string_view input) -> JSON; +/// @ingroup json +/// +/// Create a JSON document from a JSON string, returning no value instead of +/// throwing when the input is not valid JSON. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto document{sourcemeta::core::try_parse_json("[ 1, 2, 3 ]")}; +/// assert(document.has_value()); +/// assert(document.value().is_array()); +/// assert(!sourcemeta::core::try_parse_json("[ 1, 2,").has_value()); +/// ``` +SOURCEMETA_CORE_JSON_EXPORT +auto try_parse_json( + const std::basic_string_view input) + -> std::optional; + /// @ingroup json /// /// Create a JSON document from a C++ standard input stream, passing your own diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h index e862de2a..392bae36 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h @@ -21,9 +21,11 @@ #include // std::int64_t, std::uint8_t #include // std::less, std::reference_wrapper, std::function #include // std::initializer_list +#include // std::numeric_limits #include // std::allocator #include // std::set #include // std::basic_istringstream +#include // std::out_of_range #include // std::basic_string, std::char_traits #include // std::basic_string_view #include // std::is_same_v, std::remove_cvref_t @@ -774,7 +776,9 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { } /// Get the JSON numeric document as a real number if it is not one already. - /// For example: + /// A decimal whose magnitude does not fit in a double throws + /// `std::out_of_range`, matching the behaviour of converting that decimal + /// directly. For example: /// /// ```cpp /// #include @@ -783,16 +787,21 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// const sourcemeta::core::JSON document{5}; /// assert(document.as_real() == 5.0); /// ``` - [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_real() const noexcept - -> Real { + [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_real() const -> Real { assert(this->is_number()); - return this->is_real() ? this->to_real() - : static_cast(this->to_integer()); + if (this->is_real()) { + return this->to_real(); + } else if (this->is_integer()) { + return static_cast(this->to_integer()); + } else { + return this->to_decimal().to_double(); + } } /// Get the JSON numeric document as an integer number if it is not one - /// already. If the number is a real number, truncation will take place. For - /// example: + /// already. If the number is a real number, truncation will take place. A + /// value whose magnitude does not fit in a 64-bit integer throws + /// `std::out_of_range`. For example: /// /// ```cpp /// #include @@ -801,13 +810,28 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// const sourcemeta::core::JSON document{5.3}; /// assert(document.as_integer() == 5); /// ``` - [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_integer() const noexcept + [[nodiscard]] SOURCEMETA_FORCEINLINE inline auto as_integer() const -> Integer { assert(this->is_number()); if (this->is_integer()) { return this->to_integer(); + } else if (this->is_real()) { + const auto truncated{std::trunc(this->to_real())}; + if (truncated < static_cast(std::numeric_limits::min()) || + truncated >= static_cast(std::numeric_limits::max())) { + throw std::out_of_range{ + "The real number does not fit in a 64-bit integer"}; + } + + return static_cast(truncated); } else { - return static_cast(std::trunc(this->to_real())); + const auto integral{this->to_decimal().to_integral()}; + if (!integral.is_int64()) { + throw std::out_of_range{ + "The decimal number does not fit in a 64-bit integer"}; + } + + return integral.to_int64(); } } diff --git a/vendor/core/src/core/json/json.cc b/vendor/core/src/core/json/json.cc index d4229a7f..27193d0c 100644 --- a/vendor/core/src/core/json/json.cc +++ b/vendor/core/src/core/json/json.cc @@ -8,54 +8,79 @@ #include "parser.h" #include "stringify.h" -#include // assert -#include // std::uint64_t -#include // std::filesystem -#include // std::basic_istream -#include // std::numeric_limits -#include // std::basic_ostream -#include // std::cmp_greater -#include // std::vector +#include // assert +#include // std::uint64_t +#include // std::filesystem +#include // std::basic_istream +#include // std::numeric_limits +#include // std::optional, std::nullopt +#include // std::basic_ostream +#include // std::conditional_t +#include // std::cmp_greater +#include // std::vector namespace sourcemeta::core { +template static auto internal_parse_json(const char *&cursor, const char *end, std::uint64_t &line, std::uint64_t &column, const JSON::ParseCallback &callback, const bool track_positions, JSON &output) - -> void { + -> std::conditional_t { const char *buffer_start{cursor}; // Tape entries address the input with 32-bit offsets and lengths, so a larger // input cannot be represented without truncation if (std::cmp_greater(end - cursor, std::numeric_limits::max())) { - throw JSONParseError(line, column); + if constexpr (should_throw) { + throw JSONParseError(line, column); + } else { + return false; + } } std::vector tape; tape.reserve(static_cast(end - cursor) / 8); - if (callback || track_positions) { - scan_json(cursor, end, buffer_start, line, column, tape); + + if constexpr (should_throw) { + if (callback || track_positions) { + scan_json(cursor, end, buffer_start, line, column, tape); + } else { + // Re-scan with position tracking on failure for a precise error message + try { + scan_json(cursor, end, buffer_start, line, column, tape); + } catch (const JSONParseError &) { + cursor = buffer_start; + tape.clear(); + line = 1; + column = 0; + scan_json(cursor, end, buffer_start, line, column, tape); + } + } + construct_json(buffer_start, tape, callback, output); } else { + // Both the scanning and the construction phases signal failure by throwing, + // so a single boundary around them reports either as no value try { - scan_json(cursor, end, buffer_start, line, column, tape); + if (callback || track_positions) { + scan_json(cursor, end, buffer_start, line, column, tape); + } else { + scan_json(cursor, end, buffer_start, line, column, tape); + } + construct_json(buffer_start, tape, callback, output); } catch (const JSONParseError &) { - cursor = buffer_start; - tape.clear(); - line = 1; - column = 0; - scan_json(cursor, end, buffer_start, line, column, tape); + return false; } + return true; } - construct_json(buffer_start, tape, callback, output); } static auto internal_parse_json(const char *&cursor, const char *end, std::uint64_t &line, std::uint64_t &column, const bool track_positions) -> JSON { JSON output{nullptr}; - internal_parse_json(cursor, end, line, column, nullptr, track_positions, - output); + internal_parse_json(cursor, end, line, column, nullptr, track_positions, + output); return output; } @@ -110,6 +135,21 @@ auto parse_json( false); } +auto try_parse_json( + const std::basic_string_view input) + -> std::optional { + std::uint64_t line{1}; + std::uint64_t column{0}; + const char *cursor{input.empty() ? "" : input.data()}; + JSON output{nullptr}; + if (internal_parse_json(cursor, cursor + input.size(), line, column, + nullptr, false, output)) { + return output; + } + + return std::nullopt; +} + auto read_json(const std::filesystem::path &path) -> JSON { try { return parse_json(read_file_to_string(path)); @@ -127,7 +167,7 @@ auto parse_json(std::basic_istream &stream, const auto input{read_to_string(stream)}; const char *cursor{input.data()}; const char *end{input.data() + input.size()}; - internal_parse_json(cursor, end, line, column, callback, true, output); + internal_parse_json(cursor, end, line, column, callback, true, output); if (start_position != static_cast(-1)) { const auto consumed{static_cast(cursor - input.data())}; stream.clear(); @@ -140,8 +180,8 @@ auto parse_json( std::uint64_t &line, std::uint64_t &column, JSON &output, const JSON::ParseCallback &callback) -> void { const char *cursor{input.empty() ? "" : input.data()}; - internal_parse_json(cursor, cursor + input.size(), line, column, callback, - true, output); + internal_parse_json(cursor, cursor + input.size(), line, column, + callback, true, output); } // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) @@ -153,7 +193,7 @@ auto parse_json(std::basic_istream &stream, const char *end{input.data() + input.size()}; std::uint64_t line{1}; std::uint64_t column{0}; - internal_parse_json(cursor, end, line, column, callback, false, output); + internal_parse_json(cursor, end, line, column, callback, false, output); if (start_position != static_cast(-1)) { const auto consumed{static_cast(cursor - input.data())}; stream.clear(); @@ -167,8 +207,8 @@ auto parse_json( std::uint64_t line{1}; std::uint64_t column{0}; const char *cursor{input.empty() ? "" : input.data()}; - internal_parse_json(cursor, cursor + input.size(), line, column, callback, - false, output); + internal_parse_json(cursor, cursor + input.size(), line, column, + callback, false, output); } auto read_json(const std::filesystem::path &path, JSON &output, diff --git a/vendor/core/src/core/json/json_value.cc b/vendor/core/src/core/json/json_value.cc index 5231525f..26f9dfa0 100644 --- a/vendor/core/src/core/json/json_value.cc +++ b/vendor/core/src/core/json/json_value.cc @@ -432,6 +432,9 @@ auto JSON::size(const String &value) noexcept -> std::size_t { return result; } +// `as_real` is reached only for integer and real operands here, never a +// decimal, so it cannot throw even though it is no longer noexcept +// NOLINTNEXTLINE(bugprone-exception-escape) auto JSON::operator<(const JSON &other) const noexcept -> bool { if ((this->type() == Type::Integer && other.type() == Type::Real) || (this->type() == Type::Real && other.type() == Type::Integer)) { @@ -489,6 +492,9 @@ auto JSON::operator>=(const JSON &other) const noexcept -> bool { return *this > other || *this == other; } +// `as_real` is reached only for integer and real operands here, never a +// decimal, so it cannot throw even though it is no longer noexcept +// NOLINTNEXTLINE(bugprone-exception-escape) auto JSON::operator==(const JSON &other) const noexcept -> bool { if ((this->type() == Type::Integer && other.type() == Type::Real) || (this->type() == Type::Real && other.type() == Type::Integer)) { diff --git a/vendor/core/src/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/core/unicode/include/sourcemeta/core/unicode.h b/vendor/core/src/core/unicode/include/sourcemeta/core/unicode.h index e84387f6..a2f4f689 100644 --- a/vendor/core/src/core/unicode/include/sourcemeta/core/unicode.h +++ b/vendor/core/src/core/unicode/include/sourcemeta/core/unicode.h @@ -12,8 +12,8 @@ #include // std::istream #include // std::optional #include // std::ostream -#include // std::string, std::u32string -#include // std::string_view +#include // std::string, std::u32string, std::wstring +#include // std::string_view, std::wstring_view /// @defgroup unicode Unicode /// @brief Unicode encoding utilities. @@ -106,6 +106,33 @@ SOURCEMETA_CORE_UNICODE_EXPORT auto utf8_to_utf32(const std::string_view input) -> std::optional; +/// @ingroup unicode +/// Convert a UTF-8 string into its wide character form without validation. +/// The input must be valid UTF-8, otherwise the result is undefined. +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::utf8_to_wide("hello") == L"hello"); +/// ``` +SOURCEMETA_CORE_UNICODE_EXPORT +auto utf8_to_wide(const std::string_view input) -> std::wstring; + +/// @ingroup unicode +/// Convert a wide string into its UTF-8 form without validation. The input +/// must be valid, otherwise the result is undefined. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::wide_to_utf8(L"hello") == "hello"); +/// ``` +SOURCEMETA_CORE_UNICODE_EXPORT +auto wide_to_utf8(const std::wstring_view input) -> std::string; + /// @ingroup unicode /// Determine the byte length encoded by a UTF-8 lead byte. Returns 1 for an /// ASCII byte (%x00-7F), 2 for a 2-byte lead (%xC2-DF), 3 for a 3-byte lead diff --git a/vendor/core/src/core/unicode/unicode.cc b/vendor/core/src/core/unicode/unicode.cc index f3d9463a..0cba16c1 100644 --- a/vendor/core/src/core/unicode/unicode.cc +++ b/vendor/core/src/core/unicode/unicode.cc @@ -1,10 +1,19 @@ #include -#include // std::array -#include // assert -#include // std::size_t -#include // std::uint8_t -#include // std::optional, std::nullopt +#include // std::array +#include // assert +#include // std::size_t +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string, std::wstring +#include // std::string_view, std::wstring_view + +#if defined(_WIN32) || defined(__CYGWIN__) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include // std::numeric_limits +#include // MultiByteToWideChar, WideCharToMultiByte, CP_UTF8 +#endif #include "unicode_data.h" @@ -166,6 +175,92 @@ auto utf8_to_utf32(const std::string_view input) return result; } +auto utf8_to_wide(const std::string_view input) -> std::wstring { + if (input.empty()) { + return L""; + } + +#if defined(_WIN32) || defined(__CYGWIN__) + assert(input.size() <= + static_cast(std::numeric_limits::max())); + const auto size{MultiByteToWideChar( + CP_UTF8, 0, input.data(), static_cast(input.size()), nullptr, 0)}; + std::wstring result(static_cast(size), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, input.data(), static_cast(input.size()), + result.data(), size); + return result; +#else + // Outside of Windows, a wide character holds an entire codepoint, so the + // result never has more characters than the input has bytes + static_assert(sizeof(wchar_t) >= 4, + "a wide character must hold an entire codepoint"); + std::wstring result(input.size(), L'\0'); + std::size_t write{0}; + std::size_t read{0}; + while (read < input.size()) { + const auto lead{static_cast(input[read])}; + if (lead < 0x80) { + result[write++] = static_cast(lead); + read += 1; + } else if (lead < 0xE0) { + result[write++] = static_cast( + ((lead & 0x1FU) << 6U) | + (static_cast(input[read + 1]) & 0x3FU)); + read += 2; + } else if (lead < 0xF0) { + result[write++] = static_cast( + ((lead & 0x0FU) << 12U) | + ((static_cast(input[read + 1]) & 0x3FU) << 6U) | + (static_cast(input[read + 2]) & 0x3FU)); + read += 3; + } else { + result[write++] = static_cast( + ((lead & 0x07U) << 18U) | + ((static_cast(input[read + 1]) & 0x3FU) << 12U) | + ((static_cast(input[read + 2]) & 0x3FU) << 6U) | + (static_cast(input[read + 3]) & 0x3FU)); + read += 4; + } + } + + result.resize(write); + return result; +#endif +} + +auto wide_to_utf8(const std::wstring_view input) -> std::string { + if (input.empty()) { + return ""; + } + +#if defined(_WIN32) || defined(__CYGWIN__) + assert(input.size() <= + static_cast(std::numeric_limits::max())); + const auto size{WideCharToMultiByte(CP_UTF8, 0, input.data(), + static_cast(input.size()), nullptr, + 0, nullptr, nullptr)}; + std::string result(static_cast(size), '\0'); + WideCharToMultiByte(CP_UTF8, 0, input.data(), static_cast(input.size()), + result.data(), size, nullptr, nullptr); + return result; +#else + static_assert(sizeof(wchar_t) >= 4, + "a wide character must hold an entire codepoint"); + std::size_t size{0}; + for (const auto character : input) { + size += utf8_codepoint_byte_count(static_cast(character)); + } + + std::string result; + result.reserve(size); + for (const auto character : input) { + codepoint_to_utf8(static_cast(character), result); + } + + return result; +#endif +} + auto combining_class(const char32_t codepoint) noexcept -> std::uint8_t { if (codepoint > 0x10FFFF) { return 0; 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/numeric/include/sourcemeta/core/numeric_uint128.h b/vendor/core/src/lang/numeric/include/sourcemeta/core/numeric_uint128.h index df07a78d..aea02f47 100644 --- a/vendor/core/src/lang/numeric/include/sourcemeta/core/numeric_uint128.h +++ b/vendor/core/src/lang/numeric/include/sourcemeta/core/numeric_uint128.h @@ -55,17 +55,35 @@ struct uint128_t { return *this; } + auto operator-=(const uint128_t &other) noexcept -> uint128_t & { + const auto old_low = this->low; + this->low -= other.low; + this->high -= other.high + (old_low < other.low ? 1 : 0); + return *this; + } + auto operator*=(const uint128_t &other) noexcept -> uint128_t & { *this = *this * other; return *this; } + auto operator%=(const uint128_t &other) noexcept -> uint128_t & { + *this = *this % other; + return *this; + } + friend auto operator+(uint128_t left, const uint128_t &right) noexcept -> uint128_t { left += right; return left; } + friend auto operator-(uint128_t left, const uint128_t &right) noexcept + -> uint128_t { + left -= right; + return left; + } + friend auto operator*(const uint128_t &left, const uint128_t &right) noexcept -> uint128_t { std::uint64_t result_high; diff --git a/vendor/core/src/lang/text/include/sourcemeta/core/text.h b/vendor/core/src/lang/text/include/sourcemeta/core/text.h index 9c5766bc..a5dab4ff 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,55 @@ 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 whether two strings are equal under ASCII case-insensitive +/// comparison. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::equals_ignore_case("Hello", "hELLO")); +/// assert(!sourcemeta::core::equals_ignore_case("foo", "bar")); +/// ``` +SOURCEMETA_CORE_TEXT_EXPORT +auto equals_ignore_case(const std::string_view left, + const std::string_view right) noexcept -> bool; + /// @ingroup text /// /// Return `input` with `suffix` removed from the end under ASCII diff --git a/vendor/core/src/lang/text/text.cc b/vendor/core/src/lang/text/text.cc index e7e95add..6c2c6efa 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)}; @@ -134,6 +174,21 @@ auto split_once(const std::string_view input, return std::pair{before, after}; } +auto equals_ignore_case(const std::string_view left, + const std::string_view right) noexcept -> bool { + if (left.size() != right.size()) { + return false; + } + + for (std::size_t index{0}; index < left.size(); ++index) { + if (to_lowercase(left[index]) != to_lowercase(right[index])) { + return false; + } + } + + return true; +} + auto remove_suffix_ignore_case(const std::string_view input, const std::string_view suffix) noexcept -> std::string_view { @@ -153,4 +208,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