SERVER-115288 Introduce SignatureValidator (#47624)
GitOrigin-RevId: 733a6b648df19156dc4b7aca72d11ffbcd496135
This commit is contained in:
parent
f5b27c20ad
commit
d43d364bbf
@ -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()
|
||||
|
||||
14
bazel/mongot_extension_signing_key/BUILD.bazel
Normal file
14
bazel/mongot_extension_signing_key/BUILD.bazel
Normal file
@ -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"],
|
||||
)
|
||||
8
bazel/mongot_extension_signing_key/OWNERS.yml
Normal file
8
bazel/mongot_extension_signing_key/OWNERS.yml
Normal file
@ -0,0 +1,8 @@
|
||||
version: 1.0.0
|
||||
filters:
|
||||
- "*":
|
||||
approvers:
|
||||
- 10gen/query-integration-extensions-api
|
||||
- "OWNERS.yml":
|
||||
approvers:
|
||||
- 10gen/query-integration-staff-leads
|
||||
@ -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
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*
|
||||
* 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 <string_view>
|
||||
|
||||
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()
|
||||
137
bazel/mongot_extension_signing_key/gpg_export_armored_key.py
Normal file
137
bazel/mongot_extension_signing_key/gpg_export_armored_key.py
Normal file
@ -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 <gpg> <key> <passphrase_file_or_empty> <armored_key_output_file>",
|
||||
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))
|
||||
@ -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: <gpg> <key> <passphrase_or_empty> <armored_key_output_file>
|
||||
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",
|
||||
),
|
||||
},
|
||||
)
|
||||
@ -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({
|
||||
|
||||
@ -50,6 +50,10 @@
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#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<std::string>& 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<std::unique_ptr<SharedLibrary>> 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<std::string, ExtensionConfig> ExtensionLoader::getLoadedExtensions() {
|
||||
stdx::unordered_map<std::string, ExtensionConfig> result;
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
#include "mongo/stdx/unordered_map.h"
|
||||
#include "mongo/util/modules.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include <yaml-cpp/yaml.h>
|
||||
@ -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
|
||||
|
||||
@ -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 <filesystem>
|
||||
|
||||
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<ExpressionContextForTest>()) {}
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
114
src/mongo/db/extension/host/signature_validator.cpp
Normal file
114
src/mongo/db/extension/host/signature_validator.cpp
Normal file
@ -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
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*
|
||||
* 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 <fstream>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#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<char>(in)),
|
||||
std::istreambuf_iterator<char>());
|
||||
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
|
||||
70
src/mongo/db/extension/host/signature_validator.h
Normal file
70
src/mongo/db/extension/host/signature_validator.h
Normal file
@ -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
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*
|
||||
* 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 <string>
|
||||
|
||||
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
|
||||
@ -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"],
|
||||
)
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user