SERVER-115288 Introduce SignatureValidator (#47624)

GitOrigin-RevId: 733a6b648df19156dc4b7aca72d11ffbcd496135
This commit is contained in:
Santiago Roche
2026-02-18 10:26:48 -05:00
committed by MongoDB Bot
parent f5b27c20ad
commit d43d364bbf
15 changed files with 645 additions and 51 deletions

View 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"],
)

View File

@@ -0,0 +1,8 @@
version: 1.0.0
filters:
- "*":
approvers:
- 10gen/query-integration-extensions-api
- "OWNERS.yml":
approvers:
- 10gen/query-integration-staff-leads

View File

@@ -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()

View 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))

View File

@@ -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 bundles 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",
),
},
)