SERVER-115285 Provide bazel mechanism for signing test extensions (#45997)

Co-authored-by: Daniel Moody <dmoody256@gmail.com>
GitOrigin-RevId: 1377939ce9f1ef1269ec5dbddaa82d1376517fe9
This commit is contained in:
Santiago Roche 2026-01-20 17:36:52 -05:00 committed by MongoDB Bot
parent c2ae2cefc9
commit a0df8f66d5
12 changed files with 899 additions and 35 deletions

View File

@ -199,3 +199,7 @@ poetry(
load("//bazel/format:shfmt.bzl", "shfmt")
shfmt()
load("//bazel/gpg:gpg.bzl", "gpg")
gpg()

View File

@ -34,6 +34,13 @@ py_binary(
],
)
py_binary(
name = "gpg_signer",
srcs = ["gpg_signer.py"],
main = "gpg_signer.py",
visibility = ["//visibility:public"],
)
filegroup(
name = "test_wrapper",
srcs = ["test_wrapper.sh"],

1
bazel/gpg/BUILD.bazel Normal file
View File

@ -0,0 +1 @@
package(default_visibility = ["//visibility:public"])

79
bazel/gpg/gpg.bzl Normal file
View File

@ -0,0 +1,79 @@
"""Repository rules for gpg bundle download"""
load("//bazel:utils.bzl", "retry_download_and_extract")
load("@bazel_rules_mongo//utils:platforms_normalize.bzl", "ARCH_NORMALIZE_MAP", "OS_NORMALIZE_MAP")
URLS_MAP = {
"linux_aarch64": {
"sha": "d7197d8b8ad4dc4ef6c27eb03c6cc565e00f994b33011da89f37dacc92810228",
"url": "https://mdb-build-public.s3.us-east-1.amazonaws.com/gpg-binaries/SERVER-115285/gpg_bundle-aarch64.tar.gz",
},
"linux_x86_64": {
"sha": "66608c5dcfd4580ec7e7dfcf8dd16df73b563674222bf3b9785d853b3d2052ee",
"url": "https://mdb-build-public.s3.us-east-1.amazonaws.com/gpg-binaries/SERVER-115285/gpg_bundle-x86_64.tar.gz",
},
"linux_s390x": {
"sha": "1fff70fce14abfa83b08df7465929c0b98e5c12c7bff001c4fbd82adaf82c8bd",
"url": "https://mdb-build-public.s3.us-east-1.amazonaws.com/gpg-binaries/SERVER-115285/gpg_bundle-s390x.tar.gz",
},
"linux_ppc64le": {
"sha": "3f2ecdfb99c49d148f92973e5164821de663984e5a02cd6a1686ce32f1c1d9f9",
"url": "https://mdb-build-public.s3.us-east-1.amazonaws.com/gpg-binaries/SERVER-115285/gpg_bundle-ppc64le.tar.gz",
},
}
def _gpg_bundle_repo_impl(ctx):
os = ctx.os.name
os_norm = OS_NORMALIZE_MAP.get(ctx.os.name)
if os_norm != "linux":
ctx.file(
"BUILD.bazel",
content = """package(default_visibility = ["//visibility:public"])
filegroup(name = "gpg_bins", srcs = glob([]))
filegroup(name = "gpg_libs", srcs = glob([]))
""",
)
return
arch = ctx.os.arch
os_constraint = OS_NORMALIZE_MAP[os]
arch_constraint = ARCH_NORMALIZE_MAP[arch]
platform_key = "{os}_{arch}".format(os = os_constraint, arch = arch_constraint)
if platform_key not in URLS_MAP:
fail("Unsupported platform for gpg bundle: {k}. Supported: {supported}".format(
k = platform_key,
supported = ", ".join(sorted(URLS_MAP.keys())),
))
platform_info = URLS_MAP[platform_key]
ctx.report_progress("downloading gpg bundle")
retry_download_and_extract(
ctx = ctx,
tries = 5,
url = platform_info["url"],
sha256 = platform_info["sha"],
)
# BUILD file: include all bin/* and libs/** in runfiles
ctx.file(
"BUILD.bazel",
content = """package(default_visibility = ["//visibility:public"])
filegroup(
name = "gpg_libs",
srcs = glob(["gpg_bundle-*/libs/**"]),)
filegroup(
name = "gpg_bins",
srcs = glob(["gpg_bundle-*/bin/*"]),)
""",
)
_gpg_bundle_repo = repository_rule(
implementation = _gpg_bundle_repo_impl,
attrs = {},
)
def gpg():
_gpg_bundle_repo(name = "gpg")

132
bazel/gpg_signer.py Normal file
View File

@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Action helper to sign a file with a provided private key using GPG.
This replaces the inline `_gpg_sign.sh` that `bazel/signing.bzl` used to generate.
Args (positional):
1) GPG: path to the gpg binary to execute
2) KEY: path to the ascii-armored private key file to import
3) PASSPHRASE: optional path to a file containing the passphrase (or "" if none)
4) OUT: output signature path
5) INP: input file path to sign
"""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
import tempfile
from typing import List, Optional
def _debug(msg: str) -> None:
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
)
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_signer.py")
if len(argv) != 6:
print(
"usage: gpg_signer.py <gpg> <key> <passphrase_file_or_empty> <out> <inp>",
file=sys.stderr,
)
return 2
gpg = argv[1]
key = argv[2]
passphrase_file = argv[3] or None
out_path = argv[4]
inp_path = argv[5]
# 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"
)
_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])
# 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
# Build passphrase options if provided.
pass_opts: List[str] = []
if passphrase_file:
pass_opts = ["--pinentry-mode", "loopback", "--passphrase-file", passphrase_file]
_run(
[
gpg,
"--homedir",
gpgdir,
"--batch",
"--yes",
*pass_opts,
"--detach-sign",
"-u",
fpr,
"-o",
out_path,
inp_path,
]
)
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))

191
bazel/signing.bzl Normal file
View File

@ -0,0 +1,191 @@
"""Custom signing macros for test extensions."""
load("//bazel:mongo_src_rules.bzl", "mongo_cc_extension_shared_library")
def _gpg_sign_impl(ctx):
outs = []
python = ctx.toolchains["@bazel_tools//tools/python:toolchain_type"].py3_runtime
for src in ctx.files.srcs:
out = ctx.actions.declare_file(src.basename + ".sig")
outs.append(out)
# Inputs to this action
inputs = [src, ctx.file.key, ctx.file._gpg_signer]
pass_arg = ""
if ctx.file.passphrase:
inputs.append(ctx.file.passphrase)
pass_arg = ctx.file.passphrase.path
dep_files = ctx.files.gpg_bins
dep_dirs = [f.dirname for f in dep_files]
# Prefer explicit override (if provided). Otherwise, locate the real gpg binary from the bundle.
gpg_file = ctx.file.gpg_main_script
if gpg_file == None:
for f in dep_files:
if f.basename == "gpg" or f.basename == "gpg.exe":
gpg_file = f
break
if gpg_file == None:
fail("Unable to find gpg in gpg_bins. Ensure @gpg//:gpg_bins contains a 'gpg' binary.")
env = dict(ctx.configuration.default_shell_env)
sep = ctx.configuration.host_path_separator # ":" or ";"
base_path = env.get("PATH", "")
env["PATH"] = sep.join(dep_dirs + ([base_path] if base_path else []))
inputs += dep_files
# Needed for remote execution: gpg binaries use RUNPATH=$ORIGIN/../libs.
inputs += ctx.files.gpg_libs
inputs += python.files.to_list()
# Run the signer via the hermetic python toolchain (no shell involved)
ctx.actions.run(
inputs = inputs,
outputs = [out],
tools = [],
executable = python.interpreter.path,
env = env,
arguments = [
ctx.file._gpg_signer.path,
gpg_file.path, # $1
ctx.file.key.path, # $2
pass_arg, # $3 (empty if none)
out.path, # $4
src.path, # $5,
],
progress_message = "Signing {}".format(src.basename),
mnemonic = "GpgSign",
)
return [
DefaultInfo(files = depset(outs)),
OutputGroupInfo(signatures = depset(outs), originals = depset(ctx.files.srcs)),
]
gpg_sign = rule(
implementation = _gpg_sign_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
"key": attr.label(allow_single_file = True, mandatory = True),
"passphrase": attr.label(allow_single_file = True),
"gpg_main_script": attr.label(
allow_single_file = True,
doc = "Optional override for the gpg binary path. If unset, uses @gpg//:gpg_bins to locate 'gpg'.",
),
"gpg_bins": attr.label(default = Label("@gpg//:gpg_bins")),
"gpg_libs": attr.label(default = Label("@gpg//:gpg_libs")),
"_gpg_signer": attr.label(
allow_single_file = True,
default = Label("//bazel:gpg_signer.py"),
),
},
toolchains = ["@bazel_tools//tools/python:toolchain_type"],
fragments = ["py"],
)
# Extensions must be signed in order to be loaded into the server. This macros allows users to build
# the extension shared object, and sign it with the provided PGP key. The target for the packaged
# output is name + "_signed_lib". It's necessary to reference this target as a dependency to ensure
# the signed artifacts are generated.
def signed_mongo_cc_extension_shared_library(
name,
srcs = [],
deps = [],
private_hdrs = [],
visibility = None,
data = [],
tags = [],
copts = [],
linkopts = [],
includes = [],
linkstatic = False,
local_defines = [],
target_compatible_with = [],
defines = [],
additional_linker_inputs = [],
features = [],
exec_properties = {},
# signing
# path to the GPG key, we are currently going to get this from the mongo repo
key = "//src/mongo/db/extension/test_examples/test_extensions_signing_keys:test_extensions_signing_private_key.asc",
passphrase = None,
sign_visibility = None,
sign_tags = None,
**kwargs):
"""Build an extension shared library and sign it with a temporary GPG homedir.
Args:
name: Name of the extension shared library target.
srcs: Sources for the extension library.
deps: Dependencies for the extension library.
private_hdrs: Private headers for the extension library.
visibility: Visibility for created targets.
data: Runtime data deps for the extension library.
tags: Tags for created targets.
copts: C/C++ compile options for the extension library.
linkopts: Link options for the extension library.
includes: Include paths for the extension library.
linkstatic: Whether to link statically (see underlying rule semantics).
local_defines: Local preprocessor defines for the extension library.
target_compatible_with: Platform constraints for created targets.
defines: Preprocessor defines for the extension library.
additional_linker_inputs: Extra linker inputs for the extension library.
features: Bazel features to enable/disable on the extension library.
exec_properties: Exec properties for created actions.
key: Label of the signing key file (private key).
passphrase: Optional label of a file containing the signing key passphrase.
sign_visibility: Optional visibility override for signing targets.
sign_tags: Optional tags for signing targets.
**kwargs: Forwarded to the underlying extension rule.
"""
if key == None:
fail("signed_mongo_cc_extension_shared_library requires a pgp key")
sig_name = name + "_sig"
# 1) Build the shared object
mongo_cc_extension_shared_library(
name = name,
srcs = srcs,
deps = deps,
private_hdrs = private_hdrs,
visibility = visibility,
data = data,
tags = tags,
copts = copts,
linkopts = linkopts,
includes = includes,
linkstatic = linkstatic,
local_defines = local_defines,
target_compatible_with = target_compatible_with,
defines = defines,
additional_linker_inputs = additional_linker_inputs,
features = features,
exec_properties = exec_properties,
**kwargs
)
# 2) Sign the produced library (ctx.files.srcs for a rule label includes its default outputs)
gpg_sign(
name = sig_name,
srcs = [":" + name],
key = key,
passphrase = passphrase,
visibility = sign_visibility if sign_visibility != None else visibility,
tags = (sign_tags if sign_tags != None else []),
)
# 3) Aggregate both files under name + "_signed_lib" for consumption
signed_bundle_name = name + "_signed_lib"
native.filegroup(
name = signed_bundle_name,
srcs = [":" + name, ":" + sig_name],
visibility = visibility,
tags = tags,
)

View File

@ -0,0 +1,75 @@
# mongo gpg builds
This directory contains a script to produce **portable `gpg` binaries** for all our supported linux platforms:
- **Linux** (`manylinux2014` glibc 2.17 baseline): `x86_64`, `aarch64`, `s390x`, `ppc64le`
In particular, it builds gnupg-2.5.16 from source.
This script is used to generate the binaries that we use bring into bazel as a dependency to sign test extensions.
All artifacts are placed in the `dist/` directory.
---
## 📁 Contents
| Script | Platform | Output |
| :----------------------- | :-------------------------------------- | :-------------------------- |
| `build_gpg_manylinux.sh` | Linux (x86_64, aarch64, s390x, ppc64le) | `dist/gpg-manylinux-<arch>` |
---
## 🚀 Quick Start
### 🐧 Linux (manylinux2014 glibc 2.17)
**Requirements:** Docker.
To cross-build using QEMU (for aarch64/s390x/ppc64le), enable binfmt once:
```bash
docker run --privileged --rm tonistiigi/binfmt --install all
```
#### Build native architecture
```bash
./build_gpg_manylinux.sh
```
#### Cross-build via QEMU
```bash
ARCH=x86_64 PLATFORM=linux/amd64 ./build_gpg_manylinux.sh
ARCH=aarch64 PLATFORM=linux/arm64 ./build_gpg_manylinux.sh
ARCH=s390x PLATFORM=linux/s390x ./build_gpg_manylinux.sh
ARCH=ppc64le PLATFORM=linux/ppc64le ./build_gpg_manylinux.sh
```
---
## ⚙️ Build Behavior (All Platforms)
---
## 🧩 Environment Variables
| Variable | Purpose | Default |
| :--------- | :---------------- | :--------- |
| `OUT_DIR` | Output directory | `./dist` |
| `ARCH` | Linux target arch | `uname -m` |
| `PLATFORM` | Docker platform | auto |
## 📜 License & Attribution
These scripts build **gpg** and its required dependencies from sources originally obtained from:
👉 <https://www.gnupg.org/ftp/gcrypt/gnupg/> and <https://gnupg.org/download/index.html>
The exact sources can be obtained at the following URLs:
- https://mdb-build-public.s3.us-east-1.amazonaws.com/gpg-binaries/SERVER-115285/sources/gnupg-w32-2.5.16_20251230.tar.xz
- https://mdb-build-public.s3.us-east-1.amazonaws.com/gpg-binaries/SERVER-115285/sources/libassuan-3.0.2.tar.bz2
- https://mdb-build-public.s3.us-east-1.amazonaws.com/gpg-binaries/SERVER-115285/sources/libgcrypt-1.11.2.tar.bz2
- https://mdb-build-public.s3.us-east-1.amazonaws.com/gpg-binaries/SERVER-115285/sources/libgpg-error-1.58.tar.bz2
- https://mdb-build-public.s3.us-east-1.amazonaws.com/gpg-binaries/SERVER-115285/sources/libksba-1.6.7.tar.bz2
- https://mdb-build-public.s3.us-east-1.amazonaws.com/gpg-binaries/SERVER-115285/sources/npth-1.8.tar.bz2
- https://mdb-build-public.s3.us-east-1.amazonaws.com/gpg-binaries/SERVER-115285/sources/ntbtls-0.3.2.tar.bz2

View File

@ -0,0 +1,213 @@
#!/usr/bin/env bash
set -euo pipefail
# -------- config (overridable via env) --------------------------------------
ARCH="${ARCH:-$(uname -m)}" # x86_64 | aarch64 | s390x | ppc64le
GPG_DIR="gnupg-2.5.16"
OUT_DIR="${OUT_DIR:-$(pwd)/dist}"
PLATFORM="${PLATFORM:-}" # e.g. linux/arm64 if you want to force
DOCKER_IMAGE="" # filled below
CPU_BASELINE="${CPU_BASELINE:-}" # default per-arch below
# Map arch -> image + defaults
case "$ARCH" in
x86_64 | amd64)
ARCH=x86_64
DOCKER_IMAGE="quay.io/pypa/manylinux2014_x86_64"
CPU_BASELINE="${CPU_BASELINE:-x86-64}" # or x86-64-v2 / v3
;;
aarch64 | arm64)
ARCH=aarch64
DOCKER_IMAGE="quay.io/pypa/manylinux2014_aarch64"
CPU_BASELINE="${CPU_BASELINE:-generic}"
;;
s390x | 390x)
ARCH=s390x
DOCKER_IMAGE="quay.io/pypa/manylinux2014_s390x"
CPU_BASELINE="${CPU_BASELINE:-generic}"
;;
ppc64le | ppc)
ARCH=ppc64le
DOCKER_IMAGE="quay.io/pypa/manylinux2014_ppc64le"
CPU_BASELINE="${CPU_BASELINE:-generic}"
;;
*)
echo "Unsupported ARCH='$ARCH'. Expected x86_64|aarch64|s390x|ppc64le." >&2
exit 1
;;
esac
mkdir -p "$OUT_DIR"
echo "==> Build gpg for manylinux2014 ($ARCH)"
echo " Image: $DOCKER_IMAGE"
echo " CPU_BASELINE: $CPU_BASELINE"
[ -n "$PLATFORM" ] && echo " docker --platform: $PLATFORM"
# Compose optional --platform flag
PLATFORM_ARGS=()
[ -n "$PLATFORM" ] && PLATFORM_ARGS=(--platform "$PLATFORM")
MY_LD_FLAGS="-Wl,-rpath,\$\$ORIGIN/../libs -Wl,--enable-new-dtags"
docker run --rm -t "${PLATFORM_ARGS[@]}" \
-v "$OUT_DIR":/out \
"$DOCKER_IMAGE" \
bash -lc \
'
set -euo pipefail
echo "==> glibc baseline:"
ldd --version > /tmp/lddv && head -1 /tmp/lddv
mkdir mongo_gpg
cd mongo_gpg
GPG_ROOT_DIR=$(pwd)
mkdir gpg_bundle
mkdir gpg_bundle/bin
mkdir gpg_bundle/libs
GPG_BUNDLE_DIR=$GPG_ROOT_DIR/gpg_bundle
BUNDLE_BIN_DIR=$GPG_BUNDLE_DIR/bin
BUNDLE_LIBS_DIR=$GPG_BUNDLE_DIR/libs
# download key
echo "Downloading signing key"
curl -fL -o signature_key.asc https://gnupg.org/signature_key.asc
echo "Importing signing key"
if ! out=$(gpg --batch --no-tty --import signature_key.asc 2>&1); then
echo "Ignoring keys without valid self-signed UIDs during import"
fi
verify_gpg_sig() {
local artifact="$1" # e.g., libgpg-error-1.58.tar.bz2
local sig_url="$2" # e.g., https://gnupg.org/ftp/gcrypt/libgpg-error/libgpg-error-1.58.tar.bz2.sig
local sig_file="${3:-$(basename "$artifact").sig}" # optional override for sig filename
echo "Verifying $sig_file"
curl -fL -o "$sig_file" "$sig_url"
if ! gpg --batch --no-tty --verify "$sig_file" "$artifact"; then
echo "Signature verification failed for $artifact"
exit 1
fi
}
# libgpg-error
echo "Downloading gpg-error"
curl -fL -o libgpg-error-1.58.tar.bz2 https://www.gnupg.org/ftp/gcrypt/libgpg-error/libgpg-error-1.58.tar.bz2
#verify_gpg_sig "libgpg-error-1.58.tar.bz2" "https://gnupg.org/ftp/gcrypt/libgpg-error/libgpg-error-1.58.tar.bz2.sig"
tar -xvf libgpg-error-1.58.tar.bz2
echo "Making gpg-error"
cd libgpg-error-1.58
./configure
make -j20
make install
cp src/.libs/libgpg-error.so.0.41.1 $BUNDLE_LIBS_DIR/libgpg-error.so.0
# libgpgcrypt
cd $GPG_ROOT_DIR
echo "Downloading gpgcrypt"
curl -fL -o libgcrypt-1.11.2.tar.bz2 https://www.gnupg.org/ftp/gcrypt/libgcrypt/libgcrypt-1.11.2.tar.bz2
#verify_gpg_sig "libgcrypt-1.11.2.tar.bz2" "https://gnupg.org/ftp/gcrypt/libgcrypt/libgcrypt-1.11.2.tar.bz2.sig"
tar -xvf libgcrypt-1.11.2.tar.bz2
cd libgcrypt-1.11.2
./configure
make -j20
make install
cp src/.libs/libgcrypt.so.20.6.0 $BUNDLE_LIBS_DIR/libgcrypt.so.20
echo "Downloading libksba"
# libksba
cd $GPG_ROOT_DIR
curl -fL -o libksba-1.6.7.tar.bz2 https://www.gnupg.org/ftp/gcrypt/libksba/libksba-1.6.7.tar.bz2
#verify_gpg_sig "libksba-1.6.7.tar.bz2" "https://gnupg.org/ftp/gcrypt/libksba/libksba-1.6.7.tar.bz2.sig"
tar -xvf libksba-1.6.7.tar.bz2
cd libksba-1.6.7
./configure
make -j20
make install
cp src/.libs/libksba.so.8.14.7 $BUNDLE_LIBS_DIR/libksba.so.8
# libassuan
cd $GPG_ROOT_DIR
echo "Downloading libassuan"
curl -fL -o libassuan-3.0.2.tar.bz2 https://www.gnupg.org/ftp/gcrypt/libassuan/libassuan-3.0.2.tar.bz2
#verify_gpg_sig "libassuan-3.0.2.tar.bz2" "https://gnupg.org/ftp/gcrypt/libassuan/libassuan-3.0.2.tar.bz2.sig"
tar -xvf libassuan-3.0.2.tar.bz2
cd libassuan-3.0.2
./configure
make -j20
make install
cp src/.libs/libassuan.so.9.0.2 $BUNDLE_LIBS_DIR/libassuan.so.9
# ntbtls
echo "Downloading ntbtls"
cd $GPG_ROOT_DIR
curl -fL -o ntbtls-0.3.2.tar.bz2 https://www.gnupg.org/ftp/gcrypt/ntbtls/ntbtls-0.3.2.tar.bz2
#verify_gpg_sig "ntbtls-0.3.2.tar.bz2" "https://gnupg.org/ftp/gcrypt/ntbtls/ntbtls-0.3.2.tar.bz2.sig"
tar -xvf ntbtls-0.3.2.tar.bz2
cd ntbtls-0.3.2
./configure
make -j20
make install
cp src/.libs/libntbtls.so.0.1.3 $BUNDLE_LIBS_DIR/libntbtls.so.0
# npth
echo "Downloading npth"
cd $GPG_ROOT_DIR
curl -fL -o npth-1.8.tar.bz2 https://www.gnupg.org/ftp/gcrypt/npth/npth-1.8.tar.bz2
#verify_gpg_sig "npth-1.8.tar.bz2" "https://gnupg.org/ftp/gcrypt/npth/npth-1.8.tar.bz2.sig"
tar -xvf npth-1.8.tar.bz2
cd npth-1.8
./configure
make -j20
make install
cp src/.libs/libnpth.so.0.3.0 $BUNDLE_LIBS_DIR/libnpth.so.0
# gpg
cd $GPG_ROOT_DIR
echo "Downloading gpg"
curl -fL -o gnupg-w32-2.5.16_20251230.tar.xz https://www.gnupg.org/ftp/gcrypt/gnupg/gnupg-w32-2.5.16_20251230.tar.xz
# verify_gpg_sig "gnupg-w32-2.5.16_20251230.tar.xz" "https://gnupg.org/ftp/gcrypt/gnupg/gnupg-w32-2.5.16_20251230.tar.xz.sig"
tar -xvf gnupg-w32-2.5.16_20251230.tar.xz
echo "making gpg"
cd gnupg-w32-2.5.16
echo "Currently in path $(pwd)"
GPG_SRC_DIR=$(pwd)
echo $GPG_SRC_DIR
mkdir build
cd build
echo "Currently in path $(pwd)"
echo "running configure on path $GPG_SRC_DIR"
$GPG_SRC_DIR/configure --disable-sqlite
make -j20 LDFLAGS='"'"'-Wl,-rpath,\$$ORIGIN/../libs -Wl,--enable-new-dtags'"'"'
cp -L bin/gpg $BUNDLE_BIN_DIR/gpg
cp -L bin/gpg-agent $BUNDLE_BIN_DIR/gpg-agent
cp -L bin/gpg-card $BUNDLE_BIN_DIR/gpg-card
cp -L bin/gpgconf $BUNDLE_BIN_DIR/gpgconf
cp -L bin/gpgconf.ctl $BUNDLE_BIN_DIR/gpgconf.ctl
cp -L bin/gpg-connect-agent $BUNDLE_BIN_DIR/gpg-connect-agent
cp -L bin/gpgsm $BUNDLE_BIN_DIR/gpgsm
cp -L bin/gpgtar $BUNDLE_BIN_DIR/gpgtar
cp -L bin/gpgv $BUNDLE_BIN_DIR/gpgv
OUT_NAME=gpg_bundle-'"$ARCH"'
cp -r $GPG_BUNDLE_DIR /out/$OUT_NAME
'
echo "Built: $OUT_DIR/gpg_bundle-$ARCH"

View File

@ -1,7 +1,7 @@
load("//bazel/install_rules:install_rules.bzl", "extensions_with_config")
load("//bazel:mongo_src_rules.bzl", "mongo_cc_extension_shared_library")
load("@poetry//:dependencies.bzl", "dependency")
load("//bazel/config:render_template.bzl", "render_template")
load("//bazel:signing.bzl", "signed_mongo_cc_extension_shared_library")
package(default_visibility = ["//visibility:public"])
@ -31,34 +31,34 @@ extensions_with_config(
# null_chars_string_input.js will break as not all stages from $listMqlEntities
# will have a null character test. To resolve this, ensure that these new stages
# are added to the "skips" list in that test.
":bar_mongo_extension",
":foo_mongo_extension",
":limit_mongo_extension",
":logging_mongo_extension",
":debug_logging_mongo_extension",
":parse_options_mongo_extension",
":test_options_mongo_extension",
":toaster_mongo_extension",
":extension_errors_mongo_extension",
":shapify_mongo_extension",
":sharded_execution_serialization_mongo_extension",
":read_n_documents_mongo_extension",
":explain_mongo_extension",
":metadata_mongo_extension",
":idle_threads_mongo_extension",
":interrupt_mongo_extension",
":match_topN_mongo_extension",
":native_vector_search_mongo_extension",
":metrics_mongo_extension",
":other_metrics_mongo_extension",
":bar_mongo_extension_signed_lib",
":foo_mongo_extension_signed_lib",
":limit_mongo_extension_signed_lib",
":logging_mongo_extension_signed_lib",
":debug_logging_mongo_extension_signed_lib",
":parse_options_mongo_extension_signed_lib",
":test_options_mongo_extension_signed_lib",
":toaster_mongo_extension_signed_lib",
":extension_errors_mongo_extension_signed_lib",
":shapify_mongo_extension_signed_lib",
":sharded_execution_serialization_mongo_extension_signed_lib",
":read_n_documents_mongo_extension_signed_lib",
":explain_mongo_extension_signed_lib",
":metadata_mongo_extension_signed_lib",
":idle_threads_mongo_extension_signed_lib",
":interrupt_mongo_extension_signed_lib",
":match_topN_mongo_extension_signed_lib",
":native_vector_search_mongo_extension_signed_lib",
":metrics_mongo_extension_signed_lib",
":other_metrics_mongo_extension_signed_lib",
#################### EXTENSIONS FOR NO-PASSTHROUGH TESTS ####################
# Any extension that is just loaded in a no-passthrough test MUST NOT have the
# "_mongo_extension" suffix.
":no_symbol_bad_extension",
":duplicate_stage_descriptor_bad_extension",
":foo_extension_v2",
":vector_search_extension",
":no_symbol_bad_extension_signed_lib",
":duplicate_stage_descriptor_bad_extension_signed_lib",
":foo_extension_v2_signed_lib",
":vector_search_extension_signed_lib",
],
)
@ -71,7 +71,7 @@ pkg_name = "//" + package_name() + "/"
# Extensions under test_examples/
[
mongo_cc_extension_shared_library(
signed_mongo_cc_extension_shared_library(
name = extension_name + "_mongo_extension",
srcs = [extension_name + ".cpp"],
)
@ -86,7 +86,7 @@ pkg_name = "//" + package_name() + "/"
# Extensions under test_examples/desugar/
[
mongo_cc_extension_shared_library(
signed_mongo_cc_extension_shared_library(
name = extension_name + "_mongo_extension",
srcs = [pkg_name + "desugar:" + extension_name + ".cpp"],
)
@ -137,7 +137,7 @@ filegroup(
# Extensions under test_examples/extension_options/
[
mongo_cc_extension_shared_library(
signed_mongo_cc_extension_shared_library(
name = extension_name + "_mongo_extension",
srcs = [pkg_name + "extension_options:" + extension_name + ".cpp"],
)
@ -150,7 +150,7 @@ filegroup(
# Extensions under test_examples/loading/
[
mongo_cc_extension_shared_library(
signed_mongo_cc_extension_shared_library(
name = extension_name + "_mongo_extension",
srcs = [pkg_name + "loading:" + extension_name + ".cpp"],
)
@ -163,7 +163,7 @@ filegroup(
# Extensions under test_examples/host_services/
[
mongo_cc_extension_shared_library(
signed_mongo_cc_extension_shared_library(
name = extension_name + "_mongo_extension",
srcs = [pkg_name + "host_services:" + extension_name + ".cpp"],
)
@ -177,7 +177,7 @@ filegroup(
# Extensions under test_examples/observability/
[
mongo_cc_extension_shared_library(
signed_mongo_cc_extension_shared_library(
name = extension_name + "_mongo_extension",
srcs = [pkg_name + "observability:" + extension_name + ".cpp"],
)
@ -200,7 +200,7 @@ filegroup(
# Extensions under test_examples/fail_to_load/
# Each of these should fail startup.
[
mongo_cc_extension_shared_library(
signed_mongo_cc_extension_shared_library(
name = extension_name + "_bad_extension",
srcs = [pkg_name + "fail_to_load:" + extension_name + ".cpp"],
)
@ -224,7 +224,7 @@ filegroup(
# "foo_v2" is used for testing upgrade scenarios as the upgrade of "foo" from above. We cannot use
# the "mongo_extension" suffix since loading this extension in passthroughs with "foo" V1 should
# cause duplicate stage errors.
mongo_cc_extension_shared_library(
signed_mongo_cc_extension_shared_library(
name = "foo_extension_v2",
srcs = ["foo_v2.cpp"],
)
@ -232,12 +232,12 @@ mongo_cc_extension_shared_library(
# "vector_search" is used to test that we can override the existing $vectorSearch implementation.
# It must not have the _mongo_extension suffix so that it doesn't get loaded in the
# Extensions-enabled variant since that would break real $vectorSearch tests.
mongo_cc_extension_shared_library(
signed_mongo_cc_extension_shared_library(
name = "vector_search_extension",
srcs = ["vector_search.cpp"],
)
mongo_cc_extension_shared_library(
signed_mongo_cc_extension_shared_library(
name = "mongothost_extension",
srcs = [pkg_name + "extension_options:mongothost.cpp"],
)

View File

@ -0,0 +1,4 @@
exports_files(
["test_extensions_signing_private_key.asc"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,106 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
TNkpqaN9Bduv8xozbFUitHNTPTplns4uPF9Q
=SAT8
-----END PGP PRIVATE KEY BLOCK-----

View File

@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=Fjw3
-----END PGP PUBLIC KEY BLOCK-----