From d43d364bbfd6812bb749b00da879592a936cd5d5 Mon Sep 17 00:00:00 2001 From: Santiago Roche <69868136+sroches@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:26:48 -0500 Subject: [PATCH] SERVER-115288 Introduce SignatureValidator (#47624) GitOrigin-RevId: 733a6b648df19156dc4b7aca72d11ffbcd496135 --- WORKSPACE.bazel | 4 + .../mongot_extension_signing_key/BUILD.bazel | 14 ++ bazel/mongot_extension_signing_key/OWNERS.yml | 8 + .../generate_embedded_public_key_header.py | 68 +++++++++ .../gpg_export_armored_key.py | 137 ++++++++++++++++++ .../mongot_extension_signing_key.bzl | 89 ++++++++++++ src/mongo/db/extension/host/BUILD.bazel | 104 +++++++++---- .../db/extension/host/load_extension.cpp | 32 +++- src/mongo/db/extension/host/load_extension.h | 11 +- .../db/extension/host/load_extension_test.cpp | 29 +++- .../extension/host/load_extension_test_util.h | 4 +- .../db/extension/host/signature_validator.cpp | 114 +++++++++++++++ .../db/extension/host/signature_validator.h | 70 +++++++++ .../test_extensions_signing_keys/BUILD.bazel | 7 +- src/mongo/shell/servers.js | 5 +- 15 files changed, 645 insertions(+), 51 deletions(-) create mode 100644 bazel/mongot_extension_signing_key/BUILD.bazel create mode 100644 bazel/mongot_extension_signing_key/OWNERS.yml create mode 100644 bazel/mongot_extension_signing_key/generate_embedded_public_key_header.py create mode 100644 bazel/mongot_extension_signing_key/gpg_export_armored_key.py create mode 100644 bazel/mongot_extension_signing_key/mongot_extension_signing_key.bzl create mode 100644 src/mongo/db/extension/host/signature_validator.cpp create mode 100644 src/mongo/db/extension/host/signature_validator.h diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 850da97c37c..dde8e519b23 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -203,3 +203,7 @@ shfmt() load("//bazel/gpg:gpg.bzl", "gpg") gpg() + +load("//bazel/mongot_extension_signing_key:mongot_extension_signing_key.bzl", "mongot_extension_signing_key") + +mongot_extension_signing_key() diff --git a/bazel/mongot_extension_signing_key/BUILD.bazel b/bazel/mongot_extension_signing_key/BUILD.bazel new file mode 100644 index 00000000000..fca0e73ab91 --- /dev/null +++ b/bazel/mongot_extension_signing_key/BUILD.bazel @@ -0,0 +1,14 @@ +package(default_visibility = ["//visibility:public"]) + +py_binary( + name = "gpg_export_armored_key", + srcs = ["gpg_export_armored_key.py"], + main = "gpg_export_armored_key.py", + visibility = ["//visibility:public"], +) + +py_binary( + name = "generate_embedded_public_key_header", + srcs = ["generate_embedded_public_key_header.py"], + visibility = ["//visibility:public"], +) diff --git a/bazel/mongot_extension_signing_key/OWNERS.yml b/bazel/mongot_extension_signing_key/OWNERS.yml new file mode 100644 index 00000000000..94105dae971 --- /dev/null +++ b/bazel/mongot_extension_signing_key/OWNERS.yml @@ -0,0 +1,8 @@ +version: 1.0.0 +filters: + - "*": + approvers: + - 10gen/query-integration-extensions-api + - "OWNERS.yml": + approvers: + - 10gen/query-integration-staff-leads diff --git a/bazel/mongot_extension_signing_key/generate_embedded_public_key_header.py b/bazel/mongot_extension_signing_key/generate_embedded_public_key_header.py new file mode 100644 index 00000000000..22056791481 --- /dev/null +++ b/bazel/mongot_extension_signing_key/generate_embedded_public_key_header.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +import argparse + +FILE_HEADER = """/** +* Copyright (C) 2026-present MongoDB, Inc. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the Server Side Public License, version 1, +* as published by MongoDB, Inc. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* Server Side Public License for more details. +* +* You should have received a copy of the Server Side Public License +* along with this program. If not, see +* . +* +* As a special exception, the copyright holders give permission to link the +* code of portions of this program with the OpenSSL library under certain +* conditions as described in each individual source file and distribute +* linked combinations including the program with the OpenSSL library. You +* must comply with the Server Side Public License in all respects for +* all of the code used other than as permitted herein. If you modify file(s) +* with this exception, you may extend this exception to your version of the +* file(s), but you are not obligated to do so. If you do not wish to do so, +* delete this exception statement from your version. If you delete this +* exception statement from all source files in the program, then also delete +* it in the license file. +*/ + +// Generated file. Do not edit. +#pragma once +#include + +namespace mongo { +namespace extension{ +namespace host {\n""" + + +FILE_FOOTER = """} // namespace host +} // namespace extension +} // namespace mongo +""" + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--public_key_path", dest="public_key_path", required=True) + ap.add_argument("--embedded_key_header_path", required=True) + args = ap.parse_args() + + with open(args.public_key_path, "r") as f: + public_key_contents = f.read() + + # Write header + with open(args.embedded_key_header_path, "w") as h: + h.write(FILE_HEADER) + public_key_definition = """static constexpr std::string_view kMongoExtensionSigningPublicKey = R\"({0})\";\n""".format( + public_key_contents + ) + h.write(public_key_definition) + h.write(FILE_FOOTER) + + +if __name__ == "__main__": + main() diff --git a/bazel/mongot_extension_signing_key/gpg_export_armored_key.py b/bazel/mongot_extension_signing_key/gpg_export_armored_key.py new file mode 100644 index 00000000000..05d59277e4b --- /dev/null +++ b/bazel/mongot_extension_signing_key/gpg_export_armored_key.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Action helper to export a dearmored pgp public key in armored mode using GPG. +Args (positional): + 1) GPG: path to the gpg binary to execute + 2) KEY: path to the public key file to import + 3) PASSPHRASE: optional path to a file containing the passphrase (or "" if none) + 4) ARMORED_KEY_OUTPUT_FILE: output armored public key path +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +import tempfile +from typing import List, Optional + +debug = False # manually change to enable verbose output + + +def _debug(msg: str) -> None: + if debug: + print(msg, file=sys.stderr) + + +def _run(argv: List[str], *, capture_stdout: bool = False) -> subprocess.CompletedProcess: + if capture_stdout: + return subprocess.run( + argv, check=True, text=True, stdout=subprocess.PIPE, stderr=sys.stderr + ) + if not debug: + with open(os.devnull, "wb") as null: + return subprocess.run(argv, check=True, stdout=null, stderr=null) + else: + return subprocess.run(argv, check=True) + + +def _extract_fingerprint(colons_output: str) -> Optional[str]: + # gpg --with-colons output: lines like "fpr:::::::::FINGERPRINT:" + for line in colons_output.splitlines(): + if not line.startswith("fpr:"): + continue + parts = line.split(":") + if len(parts) > 9 and parts[9]: + return parts[9] + return None + + +def main(argv: List[str]) -> int: + _debug("Starting gpg_export_armored_key.py") + + if len(argv) != 5: + print( + "usage: gpg_export_armored_key.py ", + file=sys.stderr, + ) + return 2 + + gpg = argv[1] + key = argv[2] + passphrase_file = argv[3] or None + armored_key_output_file = argv[4] + + # Use helpers from the same bundle as `gpg` to avoid accidentally picking up system gpg-agent/gpgconf, + # especially under remote execution. + bindir = os.path.dirname(gpg) + gpg_agent = os.path.join(bindir, "gpg-agent") + gpgconf = os.path.join(bindir, "gpgconf") + + # Unique temp homedir for this action. + base_tmp = os.environ.get("TMPDIR") or os.getcwd() + gpgdir = tempfile.mkdtemp(prefix="gpg.", dir=base_tmp) + os.chmod(gpgdir, 0o700) + + try: + # Disable agent caching for this home directory. + with open(os.path.join(gpgdir, "gpg-agent.conf"), "w", encoding="utf-8") as fh: + fh.write( + "default-cache-ttl 0\n" + "max-cache-ttl 0\n" + "ignore-cache-for-signing\n" + "allow-loopback-pinentry\n" + "disable-scdaemon\n" + ) + + _debug("Starting gpg-agent") + # Inherit stdout/stderr so logs show up in action output (like the old shell script). + _run([gpg_agent, "--homedir", gpgdir, "--daemon", "--verbose"]) + _debug("gpg-agent importing key to home dir") + + # Import the private key into the temp homedir. + _run([gpg, "--homedir", gpgdir, "--batch", "--import", key]) + _debug("gpg-agent imported the key!") + + # Find fingerprint. + cp = _run([gpg, "--homedir", gpgdir, "--list-keys", "--with-colons"], capture_stdout=True) + fpr = _extract_fingerprint(cp.stdout) + if not fpr: + print( + "Failed to determine key fingerprint from gpg --with-colons output", file=sys.stderr + ) + return 1 + _debug("gpg-agent extracted fingerprint: " + fpr) + + # Build passphrase options if provided. + pass_opts: List[str] = [] + if passphrase_file: + pass_opts = ["--pinentry-mode", "loopback", "--passphrase-file", passphrase_file] + + cp = _run( + [ + gpg, + "--homedir", + gpgdir, + "--batch", + *pass_opts, + "--armor", + "--export", + fpr, + ], + capture_stdout=True, + ) + with open(armored_key_output_file, "w+") as outf: + outf.write(cp.stdout) + + return 0 + finally: + # Cleanup. + try: + subprocess.run([gpgconf, "--homedir", gpgdir, "--kill", "gpg-agent"], check=False) + finally: + shutil.rmtree(gpgdir, ignore_errors=True) + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/bazel/mongot_extension_signing_key/mongot_extension_signing_key.bzl b/bazel/mongot_extension_signing_key/mongot_extension_signing_key.bzl new file mode 100644 index 00000000000..98e6f7ec93b --- /dev/null +++ b/bazel/mongot_extension_signing_key/mongot_extension_signing_key.bzl @@ -0,0 +1,89 @@ +"""Rules for downloading and embedding mongot_extension_signing_key""" + +# This is the mongot-extension's signing public key. It is managed by garasign, and used by the +# SignatureValidator in secure builds (i.e MONGO_CONFIG_EXT_SIG_SECURE) to verify the authenticity +# of extensions before loading them into the server process. Whenever the remote file changes, the +# corresponding sha256 must be changed. + +def _impl(ctx): + ctx.download( + url = "https://pgp.mongodb.com/mongot-extension.pub", + sha256 = "2a15e6a2d9f6c0d8141dad515d9360f6cf01e1a11f7e2c3bc0820e18c5e9d0b7", + output = "mongot-extension.pub", + ) + ctx.file("BUILD.bazel", 'exports_files(["mongot-extension.pub"])') + +mongot_extension_signing_key_repo = repository_rule(implementation = _impl) + +def mongot_extension_signing_key(): + mongot_extension_signing_key_repo(name = "mongot_extension_signing_key") + +def _gpg_export_armored_key_impl(ctx): + key = ctx.file.key + armored_key_output_file = ctx.outputs.armored_key_output_file + pass_file = ctx.file.passphrase + + # Collect tool files from the filegroups + bin_files = ctx.attr.gpg_bins.files.to_list() + lib_files = ctx.attr.gpg_libs.files.to_list() + + # Find the gpg executable + gpg_bin = None + for f in bin_files: + if f.basename == "gpg": + gpg_bin = f + break + if gpg_bin == None: + fail("gpg binary not found in @gpg//:gpg_bins") + + # Compute libs dir next to the bundle’s bin dir: + # …/gpg_bundle-*/bin/gpg -> …/gpg_bundle-*/libs + p = gpg_bin.path + bin_dir = p[:p.rfind("/")] + bundle_dir = bin_dir[:bin_dir.rfind("/")] + libs_dir = bundle_dir + "/libs" + + # Arguments your Python helper expects: + args = [ + gpg_bin.path, + key.path, + pass_file.path if pass_file else "", + armored_key_output_file.path, + ] + + # Create the action; stage bins/libs as tools for the exec platform + ctx.actions.run( + executable = ctx.executable.script, + arguments = args, + inputs = [key] + ([pass_file] if pass_file else []), + tools = bin_files + lib_files + [ctx.executable.script], + outputs = [armored_key_output_file], + env = {"LD_LIBRARY_PATH": libs_dir}, + mnemonic = "GpgExportArmored", + progress_message = "Export armored key to %s" % armored_key_output_file.path, + ) + +gpg_export_armored_key = rule( + implementation = _gpg_export_armored_key_impl, + attrs = { + "key": attr.label(allow_single_file = True, mandatory = True), + "passphrase": attr.label(allow_single_file = True), + "armored_key_output_file": attr.output(mandatory = True), + "script": attr.label( + default = Label("//bazel/mongot_extension_signing_key:gpg_export_armored_key"), + executable = True, + cfg = "exec", + ), + # Treat these as tools (exec config) + "gpg_bins": attr.label( + default = Label("@gpg//:gpg_bins"), + allow_files = True, + cfg = "exec", + ), + "gpg_libs": attr.label( + default = Label("@gpg//:gpg_libs"), + allow_files = True, + cfg = "exec", + ), + }, +) diff --git a/src/mongo/db/extension/host/BUILD.bazel b/src/mongo/db/extension/host/BUILD.bazel index a202cdd1d57..44c05e55ce8 100644 --- a/src/mongo/db/extension/host/BUILD.bazel +++ b/src/mongo/db/extension/host/BUILD.bazel @@ -1,4 +1,5 @@ load("//bazel:mongo_src_rules.bzl", "mongo_cc_library", "mongo_cc_unit_test") +load("//bazel/mongot_extension_signing_key:mongot_extension_signing_key.bzl", "gpg_export_armored_key") package(default_visibility = ["//visibility:public"]) @@ -87,6 +88,43 @@ mongo_cc_library( ], ) +gpg_export_armored_key( + name = "mongot_extension_signing_key_asc", + armored_key_output_file = "mongot-extension.asc", + key = "@mongot_extension_signing_key//:mongot-extension.pub", + target_compatible_with = select({ + "@platforms//os:linux": [], + "//conditions:default": ["@platforms//:incompatible"], + }), +) + +genrule( + name = "embed_mongot_key", + srcs = [":mongot_extension_signing_key_asc"], + outs = ["mongot_extension_signing_key.h"], + cmd = "$(execpath //bazel/mongot_extension_signing_key:generate_embedded_public_key_header) --public_key_path $(location :mongot_extension_signing_key_asc) --embedded_key_header_path $@", + target_compatible_with = select({ + "@platforms//os:linux": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + tools = ["//bazel/mongot_extension_signing_key:generate_embedded_public_key_header"], +) + +mongo_cc_library( + name = "signature_validator", + srcs = [ + "signature_validator.cpp", + ], + hdrs = [":embed_mongot_key"], + target_compatible_with = select({ + "@platforms//os:linux": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + "//src/mongo:base", + ], +) + mongo_cc_library( name = "extension_loader", srcs = [ @@ -99,7 +137,10 @@ mongo_cc_library( "//src/mongo:base", "//src/mongo/db/query:query_knobs", "//src/third_party/yaml-cpp:yaml", - ], + ] + select({ + "//bazel/config:not_linux": [], + "//conditions:default": [":signature_validator"], + }), ) mongo_cc_unit_test( @@ -111,36 +152,37 @@ mongo_cc_unit_test( # Data lists the targets that must be built to generate extension shared libraries, which are # loaded in the unit tests. data = [ - "//src/mongo/db/extension/test_examples:bar_mongo_extension", - "//src/mongo/db/extension/test_examples:duplicate_stage_descriptor_bad_extension", - "//src/mongo/db/extension/test_examples:duplicate_version_bad_extension", - "//src/mongo/db/extension/test_examples:explain_mongo_extension", - "//src/mongo/db/extension/test_examples:extension_errors_mongo_extension", - "//src/mongo/db/extension/test_examples:foo_mongo_extension", - "//src/mongo/db/extension/test_examples:host_version_fails_bad_extension", - "//src/mongo/db/extension/test_examples:host_version_succeeds_mongo_extension", - "//src/mongo/db/extension/test_examples:initialize_version_fails_bad_extension", - "//src/mongo/db/extension/test_examples:limit_mongo_extension", - "//src/mongo/db/extension/test_examples:load_highest_compatible_version_mongo_extension", - "//src/mongo/db/extension/test_examples:load_two_stages_mongo_extension", - "//src/mongo/db/extension/test_examples:major_version_max_int_bad_extension", - "//src/mongo/db/extension/test_examples:major_version_too_high_bad_extension", - "//src/mongo/db/extension/test_examples:major_version_too_low_bad_extension", - "//src/mongo/db/extension/test_examples:match_topN_mongo_extension", - "//src/mongo/db/extension/test_examples:minor_version_too_high_bad_extension", - "//src/mongo/db/extension/test_examples:mongothost_extension", - "//src/mongo/db/extension/test_examples:native_vector_search_mongo_extension", - "//src/mongo/db/extension/test_examples:no_compatible_version_bad_extension", - "//src/mongo/db/extension/test_examples:no_symbol_bad_extension", - "//src/mongo/db/extension/test_examples:null_initialize_function_bad_extension", - "//src/mongo/db/extension/test_examples:null_mongo_extension_bad_extension", - "//src/mongo/db/extension/test_examples:null_stage_descriptor_bad_extension", - "//src/mongo/db/extension/test_examples:parse_options_mongo_extension", - "//src/mongo/db/extension/test_examples:read_n_documents_mongo_extension", - "//src/mongo/db/extension/test_examples:sharded_execution_serialization_mongo_extension", - "//src/mongo/db/extension/test_examples:test_options_mongo_extension", - "//src/mongo/db/extension/test_examples:toaster_mongo_extension", - "//src/mongo/db/extension/test_examples:vector_search_optimization_mongo_extension", + "//src/mongo/db/extension/test_examples:bar_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:duplicate_stage_descriptor_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:duplicate_version_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:explain_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:extension_errors_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:foo_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:host_version_fails_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:host_version_succeeds_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:initialize_version_fails_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:limit_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:load_highest_compatible_version_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:load_two_stages_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:major_version_max_int_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:major_version_too_high_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:major_version_too_low_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:match_topN_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:minor_version_too_high_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:mongothost_extension_signed_lib", + "//src/mongo/db/extension/test_examples:native_vector_search_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:no_compatible_version_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:no_symbol_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:null_initialize_function_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:null_mongo_extension_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:null_stage_descriptor_bad_extension_signed_lib", + "//src/mongo/db/extension/test_examples:parse_options_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:read_n_documents_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:sharded_execution_serialization_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:test_options_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:toaster_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples:vector_search_optimization_mongo_extension_signed_lib", + "//src/mongo/db/extension/test_examples/test_extensions_signing_keys:test_extensions_signing_public_key.asc", ], tags = ["mongo_unittest_seventh_group"], target_compatible_with = select({ diff --git a/src/mongo/db/extension/host/load_extension.cpp b/src/mongo/db/extension/host/load_extension.cpp index 3e5a67360b1..f6756ba4bb7 100644 --- a/src/mongo/db/extension/host/load_extension.cpp +++ b/src/mongo/db/extension/host/load_extension.cpp @@ -50,6 +50,10 @@ #include #include +#ifdef __linux +#include "mongo/db/extension/host/signature_validator.h" +#endif + #define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kExtension namespace mongo::extension::host { @@ -58,7 +62,7 @@ namespace { const std::filesystem::path& getExtensionConfDir() { // Use /tmp/mongo/extensions in test environments, otherwise use /etc/mongo/extensions. static const std::filesystem::path kExtensionConfDir = getTestCommandsEnabled() - ? std::filesystem::temp_directory_path() / "mongo" / "extensions" + ? std::filesystem::temp_directory_path() / ExtensionLoader::kExtensionConfigPathSuffix : ExtensionLoader::kExtensionConfigPath; return kExtensionConfDir; @@ -134,13 +138,14 @@ bool loadExtensions(const std::vector& extensionNames) { // Register fallback stub parsers before loading extensions. registerUnloadedExtensionStubParsers(); + SignatureValidator signatureValidator; for (const auto& extension : extensionNames) { LOGV2(10668501, "Loading extension", "extensionName"_attr = extension); try { const ExtensionConfig config = ExtensionLoader::loadExtensionConfig(extension); - ExtensionLoader::load(extension, config); + ExtensionLoader::load(extension, config, signatureValidator); } catch (...) { LOGV2_ERROR(10668502, "Error loading extension", @@ -203,13 +208,29 @@ ExtensionConfig ExtensionLoader::loadExtensionConfig(const std::string& extensio void ExtensionLoader::load(const std::string& name, const ExtensionConfig& config) { #ifdef __linux + SignatureValidator signatureValidator; + return ExtensionLoader::load(name, config, signatureValidator); +#else + LOGV2(11901201, + "Loading extensions on non-linux platforms is not supported - skipping loading."); +#endif +} + +#ifdef __linux +void ExtensionLoader::load(const std::string& name, + const ExtensionConfig& config, + const SignatureValidator& signatureValidator) { uassert(10845400, str::stream() << "Loading extension '" << name << "' failed: " << "Extension has already been loaded", !loadedExtensions.contains(name)); const auto& extensionPath = config.sharedLibraryPath; - + uassert(11528800, + str::stream() << "Loading extension '" << name << "' failed, path: " << extensionPath + << " does not exist.", + std::filesystem::exists(extensionPath)); + signatureValidator.validateExtensionSignature(name, extensionPath); StatusWith> swExtensionLib = SharedLibrary::create(extensionPath); uassert(10615500, @@ -244,11 +265,8 @@ void ExtensionLoader::load(const std::string& name, const ExtensionConfig& confi YAML::Dump(extOptionsWithMongotHost), std::move(hostPortal)}; extHandle->initialize(&portal, &host_connector::HostServicesAdapter::get()); -#else - LOGV2(11901201, - "Loading extensions on non-linux platforms is not supported - skipping loading."); -#endif } +#endif stdx::unordered_map ExtensionLoader::getLoadedExtensions() { stdx::unordered_map result; diff --git a/src/mongo/db/extension/host/load_extension.h b/src/mongo/db/extension/host/load_extension.h index 5befd497e93..75c8467a241 100644 --- a/src/mongo/db/extension/host/load_extension.h +++ b/src/mongo/db/extension/host/load_extension.h @@ -34,6 +34,7 @@ #include "mongo/stdx/unordered_map.h" #include "mongo/util/modules.h" +#include #include #include @@ -75,7 +76,11 @@ public: * compatibility, and calls the extension initialization function. */ static void load(const std::string& name, const ExtensionConfig& config); - +#ifdef __linux + static void load(const std::string& name, + const ExtensionConfig& config, + const class SignatureValidator& signatureValidator); +#endif /** * Returns the names and configurations of the currently registered extensions. @@ -95,11 +100,13 @@ public: */ static void unload_forTest(const std::string& name); + static inline const std::filesystem::path kExtensionConfigPathSuffix{"mongo/extensions"}; /** * Central path for all extension configuration files. Also holds * aggregation_stage_fallback_parsers.json. */ - static inline const std::filesystem::path kExtensionConfigPath{"/etc/mongo/extensions"}; + static inline const std::filesystem::path kExtensionConfigPath = + std::filesystem::path("/etc") / kExtensionConfigPathSuffix; private: // Used to keep loaded extension 'SharedLibrary' objects alive for the lifetime of the server diff --git a/src/mongo/db/extension/host/load_extension_test.cpp b/src/mongo/db/extension/host/load_extension_test.cpp index 9ac9892cd8e..732544a5777 100644 --- a/src/mongo/db/extension/host/load_extension_test.cpp +++ b/src/mongo/db/extension/host/load_extension_test.cpp @@ -33,6 +33,7 @@ #include "mongo/db/extension/host/document_source_extension_optimizable.h" #include "mongo/db/extension/host/load_extension_test_util.h" #include "mongo/db/extension/host/load_stub_parsers.h" +#include "mongo/db/extension/host/signature_validator.h" #include "mongo/db/pipeline/document_source.h" #include "mongo/db/pipeline/document_source_limit.h" #include "mongo/db/pipeline/document_source_match.h" @@ -42,14 +43,24 @@ #include "mongo/db/pipeline/lite_parsed_document_source.h" #include "mongo/db/pipeline/pipeline_factory.h" #include "mongo/db/query/search/mongot_options.h" +#include "mongo/db/server_options.h" #include "mongo/idl/server_parameter_test_controller.h" #include "mongo/unittest/death_test.h" +#include "mongo/unittest/temp_dir.h" #include "mongo/unittest/unittest.h" #include namespace mongo::extension::host { +namespace { +const std::string& getPublicKeyPath() { + static std::string kPublicKeyPath = mongo::extension::host::test_util::getExtensionDirectory() / + "test_extensions_signing_keys" / "test_extensions_signing_public_key.asc"; + return kPublicKeyPath; +} +} // namespace + class LoadExtensionsTest : public unittest::Test { protected: LoadExtensionsTest() : expCtx(make_intrusive()) {} @@ -63,7 +74,16 @@ protected: static inline const std::string kMatchTopNStageName = "$matchTopN"; static inline const std::string kMatchTopNLibExtensionPath = "libmatch_topN_mongo_extension.so"; + void setUp() override { + _previousExtensionsSignaturePublicKeyPath = + serverGlobalParams.extensionsSignaturePublicKeyPath; + serverGlobalParams.extensionsSignaturePublicKeyPath = getPublicKeyPath(); + } void tearDown() override { + if (!_previousExtensionsSignaturePublicKeyPath.empty()) { + serverGlobalParams.extensionsSignaturePublicKeyPath = + _previousExtensionsSignaturePublicKeyPath; + } LiteParsedDocumentSource::unregisterParser_forTest(kTestFooStageName); ExtensionLoader::unload_forTest("foo"); LiteParsedDocumentSource::unregisterParser_forTest(kMatchTopNStageName); @@ -92,6 +112,7 @@ protected: private: RAIIServerParameterControllerForTest _featureFlag{"featureFlagExtensionsAPI", true}; + std::string _previousExtensionsSignaturePublicKeyPath{""}; }; TEST_F(LoadExtensionsTest, LoadExtensionErrorCases) { @@ -103,22 +124,22 @@ TEST_F(LoadExtensionsTest, LoadExtensionErrorCases) { // Test that various non-existent extension cases fail with the proper error code. ASSERT_THROWS_CODE(ExtensionLoader::load("src", test_util::makeEmptyExtensionConfig("src/")), AssertionException, - 10615500); + 11528800); ASSERT_THROWS_CODE(ExtensionLoader::load("notanextension", test_util::makeEmptyExtensionConfig("notanextension")), AssertionException, - 10615500); + 11528800); ASSERT_THROWS_CODE( ExtensionLoader::load( "extension", test_util::makeEmptyExtensionConfig("path/to/nonexistent/extension.so")), AssertionException, - 10615500); + 11528800); ASSERT_THROWS_CODE( ExtensionLoader::load("notanextension", test_util::makeEmptyExtensionConfig("libnotanextension.so")), AssertionException, - 10615500); + 11528800); // no_symbol_bad_extension is missing the get_mongodb_extension symbol definition. ASSERT_THROWS_CODE( diff --git a/src/mongo/db/extension/host/load_extension_test_util.h b/src/mongo/db/extension/host/load_extension_test_util.h index f6ead95e964..b0d7a2ef30a 100644 --- a/src/mongo/db/extension/host/load_extension_test_util.h +++ b/src/mongo/db/extension/host/load_extension_test_util.h @@ -40,14 +40,14 @@ static inline const std::filesystem::path runFilesDir = std::getenv("RUNFILES_DI * Returns the directory containing extension shared libraries built for tests. */ inline std::filesystem::path getExtensionDirectory() { - return "_main/src/mongo/db/extension/test_examples"; + return runFilesDir / "_main/src/mongo/db/extension/test_examples"; } /** * Computes the absolute path to a specific test extension shared library. */ inline std::filesystem::path getExtensionPath(const std::string& extensionFileName) { - return runFilesDir / getExtensionDirectory() / extensionFileName; + return getExtensionDirectory() / extensionFileName; } /** diff --git a/src/mongo/db/extension/host/signature_validator.cpp b/src/mongo/db/extension/host/signature_validator.cpp new file mode 100644 index 00000000000..6e793754c11 --- /dev/null +++ b/src/mongo/db/extension/host/signature_validator.cpp @@ -0,0 +1,114 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + + +#include "mongo/db/extension/host/signature_validator.h" + +#include "mongo/db/extension/host/mongot_extension_signing_key.h" +#include "mongo/db/server_options.h" +#include "mongo/logv2/log.h" +#include "mongo/util/str.h" + +#include + +#include + +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kExtension + +namespace mongo::extension::host { + +namespace { + +// Note, this function should only be called if we are not skipping validation, since we expect a +// non-empty extensionValidationPublicKeyPath. +// TODO SERVER-115289: Revisit public key management depending on library implementation. +const std::string& getValidationPublicKey() { +#ifndef MONGO_CONFIG_EXT_SIG_SECURE + static const std::string kPublicKey = []() { + const auto& extensionValidationPublicKeyPath = + serverGlobalParams.extensionsSignaturePublicKeyPath; + tassert(11528801, + "extensionsSignaturePublicKeyPath was empty!", + !extensionValidationPublicKeyPath.empty()); + LOGV2_DEBUG(11528803, + 4, + "SignatureValidator using public key path", + "extensionValidationPublicKeyPath"_attr = extensionValidationPublicKeyPath); + std::ifstream in(extensionValidationPublicKeyPath); + tassert(11528802, + fmt::format("Failed to open signature file: {}", extensionValidationPublicKeyPath), + in); + std::string contents((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + return contents; + }(); +#else + static const std::string kPublicKey(kMongoExtensionSigningPublicKey); +#endif + return kPublicKey; +} +} // namespace + +SignatureValidator::SignatureValidator() + : _skipValidation([]() { +// TODO SERVER-115289: Remove ENABLE_SIGNATURE_VALIDATOR guard for skipValidation. +#ifdef ENABLE_SIGNATURE_VALIDATOR +#ifndef MONGO_CONFIG_EXT_SIG_SECURE + return serverGlobalParams.extensionsSignaturePublicKeyPath.empty(); +#else + return false; +#endif +#else + return true; +#endif + }()) { + LOGV2_DEBUG(11528804, 4, "Initializing SignatureValidator"); + + if (_skipValidation) { + LOGV2_DEBUG(11528805, 4, "Skipping signature validation"); + return; + } + // TODO SERVER-115289: Initialize implementation specific context and import key into keyring. +} + +SignatureValidator::~SignatureValidator() { + if (_skipValidation) { + return; + } +} + +void SignatureValidator::validateExtensionSignature(const std::string& extensionName, + const std::string& extensionPath) const { + if (_skipValidation) { + LOGV2_DEBUG(11528806, 4, "Skipping signature validation"); + return; + } + // TODO SERVER-115289: Implement signature validation. +} +} // namespace mongo::extension::host diff --git a/src/mongo/db/extension/host/signature_validator.h b/src/mongo/db/extension/host/signature_validator.h new file mode 100644 index 00000000000..e0890ebcc28 --- /dev/null +++ b/src/mongo/db/extension/host/signature_validator.h @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/util/modules.h" + +#include + +namespace mongo::extension::host { + +/** + * SignatureValidator is responsible for validating an extension's signature file against a public + * key. + * + * This class respects the compile-time pre-processor flag MONGO_CONFIG_EXT_SIG_SECURE and server + * options (i.e extensionsSignaturePublicKeyPath) when determining which validation public key to + * use for signature verification. Note, this class is always safe to instantiate, even if signature + * verification is disabled (i.e extensionsSignaturePublicKeyPath is empty). + * + * Note: SignatureValidator is currently always disabled for the time being. + * TODO SERVER-115289: Update comment with implementation specific details regarding signature + * verification library. + */ +class SignatureValidator { +public: + SignatureValidator(); + SignatureValidator(const SignatureValidator&) = delete; + SignatureValidator& operator=(const SignatureValidator&) = delete; + + ~SignatureValidator(); + /** + * Validates the extension's detached signature file against the validation public key. + * Note, extensionPath must be guaranteed to exist prior to calling this method. If the + * signature is not validated succesfully, an exception is thrown. + */ + void validateExtensionSignature(const std::string& extensionName, + const std::string& extensionPath) const; + +private: + const bool _skipValidation; +}; + +} // namespace mongo::extension::host diff --git a/src/mongo/db/extension/test_examples/test_extensions_signing_keys/BUILD.bazel b/src/mongo/db/extension/test_examples/test_extensions_signing_keys/BUILD.bazel index 0da0ca36682..6cdbb8d9d96 100644 --- a/src/mongo/db/extension/test_examples/test_extensions_signing_keys/BUILD.bazel +++ b/src/mongo/db/extension/test_examples/test_extensions_signing_keys/BUILD.bazel @@ -1,4 +1,9 @@ +package(default_visibility = ["//visibility:public"]) + exports_files( - ["test_extensions_signing_private_key.asc"], + [ + "test_extensions_signing_private_key.asc", + "test_extensions_signing_public_key.asc", + ], visibility = ["//visibility:public"], ) diff --git a/src/mongo/shell/servers.js b/src/mongo/shell/servers.js index 4d542520073..03465f71d3f 100644 --- a/src/mongo/shell/servers.js +++ b/src/mongo/shell/servers.js @@ -58,10 +58,7 @@ MongoRunner.getMongoShellPath = function () { }; MongoRunner.getExtensionPath = function (shared_library_name) { - if (!jsTestOptions().inEvergreen) { - return MongoRunner.getInstallPath("..", "lib", shared_library_name); - } - return shared_library_name; + return MongoRunner.getInstallPath("..", "lib", shared_library_name); }; MongoRunner.parsePort = function (...args) {