From 728d2bfee72cf7d88cd6a88a67810edd2d143aad Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Sun, 20 Jul 2025 19:55:12 +0200 Subject: [PATCH] contrib/plugins: add mTLS binary cache store plugin Adds an example plugin implementing an mTLS-enabled binary cache store (https+mtls:// scheme) using client certificates for authentication. Darwin fix: don't link liblix* into plugins (host resolves symbols at runtime via dynamic_lookup). Explicitly link curl so it binds to Nix-store libcurl, not /usr/lib/libcurl. This prevents the plugin's curl_easy_setopt calls from operating on the wrong libcurl instance. Test portability: BSD sed -i wrapper, OpenSSL -sha256 for cert signing, redirect test server output to log file. Change-Id: I652b987d3ac45e31df50ff4ba1f523294438c2b6 --- contrib/plugins/meson.build | 22 ++++ .../plugins/mtls-http-binary-cache-store.md | 14 +++ contrib/plugins/plugin_mtls_store.cc | 102 ++++++++++++++++ doc/manual/change-authors.yml | 6 + doc/manual/rl-next/mtls-plugin.md | 42 +++++++ meson.build | 18 ++- meson.options | 4 + misc/pre-commit.nix | 2 +- package.nix | 3 + .../common/vars-and-functions.sh.in | 15 ++- tests/functional/init.sh | 2 +- tests/functional/meson.build | 6 + .../mtls-nix-binary-cache-ssl-server.py | 113 ++++++++++++++++++ .../mtls-substituter-ssl-client-cert.sh | 108 +++++++++++++++++ 14 files changed, 449 insertions(+), 8 deletions(-) create mode 100644 contrib/plugins/meson.build create mode 100644 contrib/plugins/mtls-http-binary-cache-store.md create mode 100644 contrib/plugins/plugin_mtls_store.cc create mode 100644 doc/manual/rl-next/mtls-plugin.md create mode 100755 tests/functional/mtls-nix-binary-cache-ssl-server.py create mode 100755 tests/functional/mtls-substituter-ssl-client-cert.sh diff --git a/contrib/plugins/meson.build b/contrib/plugins/meson.build new file mode 100644 index 000000000..d40c646c3 --- /dev/null +++ b/contrib/plugins/meson.build @@ -0,0 +1,22 @@ +# Darwin: don't link liblix* into plugins (host process provides them at runtime). +# Explicitly link curl so it binds to Nix-store libcurl, not /usr/lib/libcurl. +if is_darwin + plugin_deps = [ + liblixutil.partial_dependency(includes : true, compile_args : true), + liblixstore.partial_dependency(includes : true, compile_args : true), + liblixexpr.partial_dependency(includes : true, compile_args : true), + liblixfetchers.partial_dependency(includes : true, compile_args : true), + curl, + ] +else + plugin_deps = [liblixutil, liblixstore, liblixexpr, liblixfetchers, curl] +endif + +plugin_mtls_store = shared_module( + 'plugin_mtls_store', + 'plugin_mtls_store.cc', + dependencies : plugin_deps, + install : false, + build_by_default : true, + link_args : is_darwin ? shared_module_link_args : strict_shared_module_link_args, +) diff --git a/contrib/plugins/mtls-http-binary-cache-store.md b/contrib/plugins/mtls-http-binary-cache-store.md new file mode 100644 index 000000000..688c780a2 --- /dev/null +++ b/contrib/plugins/mtls-http-binary-cache-store.md @@ -0,0 +1,14 @@ +R"( + +**Store URL format**: `https+mtls://...` + +This store allows a binary cache to be accessed via HTTPS with mutual TLS (client certificate authentication). + +Both parameters are required: + +- `tls-certificate`, a path to the TLS client certificate +- `tls-private-key`, a path to the TLS private key backing the client certificate + +If you don't need mTLS, use `https://` instead. + +)" diff --git a/contrib/plugins/plugin_mtls_store.cc b/contrib/plugins/plugin_mtls_store.cc new file mode 100644 index 000000000..9ae6b1017 --- /dev/null +++ b/contrib/plugins/plugin_mtls_store.cc @@ -0,0 +1,102 @@ +#include "lix/libstore/store-api.hh" +#include "lix/libutil/config.hh" +#include "lix/libstore/http-binary-cache-store.hh" +#include +#include + +namespace nix { +struct mTLSBinaryCacheStoreConfig : HttpBinaryCacheStoreConfig +{ + using HttpBinaryCacheStoreConfig::HttpBinaryCacheStoreConfig; + + const std::string name() override + { + return "mTLS HTTP Binary Cache Store"; + } + + std::string doc() override + { + return +#include "mtls-http-binary-cache-store.md" + ; + } + + PathsSetting tlsCertificate{ + this, + "", + "tls-certificate", + "Path of the TLS client certificate in PEM format as expected by CURLOPT_SSLCERT" + }; + + PathsSetting tlsKey{ + this, + "", + "tls-private-key", + "Path of the TLS client certificate private key in PEM format as expected by CURLOPT_SSLKEY" + }; +}; + +struct mTLSBinaryCacheStoreImpl : public HttpBinaryCacheStore +{ + struct Keyring + { + nix::Path tlsCertificate; + nix::Path tlsKey; + }; + + mTLSBinaryCacheStoreConfig config_; + std::shared_ptr keyring; + + mTLSBinaryCacheStoreConfig & config() override + { + return config_; + } + const mTLSBinaryCacheStoreConfig & config() const override + { + return config_; + } + + mTLSBinaryCacheStoreImpl( + const std::string & uriScheme, const Path & _cacheUri, mTLSBinaryCacheStoreConfig config + ) + : Store(config) + , HttpBinaryCacheStore("https", _cacheUri, config) + , config_(std::move(config)) + , keyring(std::make_shared(config_.tlsCertificate.get(), config_.tlsKey.get())) + { + } + + FileTransferOptions makeOptions(Headers && headers = {}) override + { + auto options = HttpBinaryCacheStore::makeOptions(std::move(headers)); + auto baseExtraSetup = std::move(options.extraSetup); + auto keyring = this->keyring; + + options.extraSetup = [keyring, baseExtraSetup{std::move(baseExtraSetup)}](CURL * req) { + if (baseExtraSetup) { + baseExtraSetup(req); + } + + const bool haveCert = !keyring->tlsCertificate.empty(); + const bool haveKey = !keyring->tlsKey.empty(); + if (!(haveCert && haveKey)) { + throw Error("https+mtls requires both tls-certificate and tls-private-key"); + } + curl_easy_setopt(req, CURLOPT_SSLCERT, keyring->tlsCertificate.c_str()); + curl_easy_setopt(req, CURLOPT_SSLKEY, keyring->tlsKey.c_str()); + }; + + return options; + } + + static std::set uriSchemes() + { + return {"https+mtls"}; + } +}; +} + +extern "C" void nix_plugin_entry() +{ + nix::StoreImplementations::add(); +} diff --git a/doc/manual/change-authors.yml b/doc/manual/change-authors.yml index fd0e37bae..b598175fc 100644 --- a/doc/manual/change-authors.yml +++ b/doc/manual/change-authors.yml @@ -198,6 +198,9 @@ nan-git: ncfavier: github: ncfavier +nkk0: + github: nkk0 + not-my-profile: display_name: Martin Fischer github: not-my-profile @@ -276,6 +279,9 @@ vigress8: forgejo: vigress8 github: vigress8 +vlaci: + github: vlaci + vlinkz: display_name: Victor Fuentes forgejo: vlinkz diff --git a/doc/manual/rl-next/mtls-plugin.md b/doc/manual/rl-next/mtls-plugin.md new file mode 100644 index 000000000..3a2839d32 --- /dev/null +++ b/doc/manual/rl-next/mtls-plugin.md @@ -0,0 +1,42 @@ +--- +synopsis: "mTLS store connections via a plugin" +issues: [] +cls: [3754, 3696, 3697, 3698] +category: Improvements +credits: [raito, horrors, mic92, vlaci, nkk0] +--- + +To support use cases requiring mutual TLS (mTLS) authentication when connecting +to remote Nix stores, e.g. private stores, we have introduced a **contributed** +mTLS plugin extending the Lix store interface. + +This design follows an extensibility model which was brought up [by a proposal +of making Kerberos authentication possible in Lix +directly](https://gerrit.lix.systems/c/lix/+/3637). + +This mTLS plugin serves as a concrete example of how store connection +mechanisms can be modularized through external plugins, without extending Lix +core. This idea can be generalized to integrate automatic certificate renewal +or advanced integrations with secrets engine or posture checks. + +It enables custom TLS client certificates to be used for authenticating against +a remote store that enforces mTLS. + +To use the plugin, configure Lix manually by setting in your `nix.conf`: + +``` +plugin-files = /a/path/to/libplugin_mtls_store.so +``` + +Currently, this must be done explicitly. In the future, Nixpkgs will provide a +mechanism to reference an up-to-date and curated set of plugins automatically. + +Making plugins easily consumable outside of Nixpkgs (e.g., from external plugin +registries or binary distributions) remains an open question and will require +further design. + +Contributed plugins come with significantly reduced **stability** and +**maintenance** guarantees compared to the Lix core. We encourage users who +depend on a given plugin to take on maintenance responsibilities and apply for +ownership within the Lix mono-repository. These plugins are subject to removal +at any time. diff --git a/meson.build b/meson.build index 837755a28..48c37135b 100644 --- a/meson.build +++ b/meson.build @@ -114,6 +114,7 @@ endif enable_nix_eval_jobs = get_option('nix-eval-jobs') enable_tests = get_option('enable-tests') +enable_contrib_plugins = get_option('enable-contrib-plugins') tests_args = [] @@ -238,13 +239,18 @@ endif # variable is here instead. # This corresponds to the $(1)_ALLOW_UNDEFINED option from the Make buildsystem. # Mostly this is load-bearing on the plugin tests defined in tests/functional/plugins/meson.build. +# +# On Darwin, plugins are two-level namespace bundles. Using -Wl,-undefined,dynamic_lookup +# allows unresolved symbols to be resolved from the host process at runtime, avoiding the +# symbol binding issues that occur with -flat_namespace (where plugin curl_* calls could +# resolve to /usr/lib/libcurl instead of the Nix-store libcurl used by the host). shared_module_link_args = [] +# This is a stricter additional set of link flags (for non-plugin shared modules). +strict_shared_module_link_args = [] if is_darwin - shared_module_link_args += ['-undefined', 'suppress', '-flat_namespace'] + shared_module_link_args += ['-Wl,-undefined,dynamic_lookup'] elif is_linux - # -Wl,-z,defs is the equivalent, but a comment in the Make buildsystem says that breaks - # Clang sanitizers on Linux. - # FIXME(Qyriad): is that true? + strict_shared_module_link_args += ['-Wl,-z,defs'] endif configdata = configuration_data() @@ -718,6 +724,10 @@ if enable_tests subdir('tests/functional2') endif +if enable_contrib_plugins + subdir('contrib/plugins') +endif + subdir('meson/clang-tidy') subproject('nix-eval-jobs', required : enable_nix_eval_jobs) diff --git a/meson.options b/meson.options index 4e46f3d7a..1a3d98cbb 100644 --- a/meson.options +++ b/meson.options @@ -40,6 +40,10 @@ option('enable-tests', type : 'boolean', value : true, description : 'whether to enable tests or not (requires rapidcheck and gtest)', ) +option('enable-contrib-plugins', type : 'boolean', value : true, + description : 'whether to build contributed plugins' +) + option('tests-color', type : 'boolean', value : true, description : 'set to false to disable color output in gtest', ) diff --git a/misc/pre-commit.nix b/misc/pre-commit.nix index 51be9cabf..44c35067c 100644 --- a/misc/pre-commit.nix +++ b/misc/pre-commit.nix @@ -36,7 +36,7 @@ pre-commit-run { enable = true; package = pkgs.llvmPackages.libclang.python; entry = "${pkgs.llvmPackages.libclang.python}/bin/git-clang-format --binary ${pkgs.llvmPackages.clang-tools}/bin/clang-format"; - files = "^(lix/|tests/)"; + files = "^(lix/|tests/|contrib/plugins/)"; types = [ "c++" "file" diff --git a/package.nix b/package.nix index 7b202b9d9..e9c645384 100644 --- a/package.nix +++ b/package.nix @@ -294,6 +294,7 @@ stdenv.mkDerivation (finalAttrs: { ./doc ./lix ./misc + ./contrib/plugins ./COPYING ] ++ lib.optionals lintInsteadOfBuild [ ./.clang-tidy ] @@ -395,6 +396,8 @@ stdenv.mkDerivation (finalAttrs: { yq lsof zstd + # For mTLS tests + curl ] ++ lib.optional useLld lldBintools ++ lib.optional hostPlatform.isLinux util-linuxMinimal diff --git a/tests/functional/common/vars-and-functions.sh.in b/tests/functional/common/vars-and-functions.sh.in index 8202402b3..0536393d0 100644 --- a/tests/functional/common/vars-and-functions.sh.in +++ b/tests/functional/common/vars-and-functions.sh.in @@ -242,9 +242,20 @@ buggyNeedLocalStore() { needLocalStore "$1" } +# BSD/macOS sed -i requires '' as separate arg; GNU sed does not. +sedInPlace() { + local expr="$1" + local file="$2" + if sed --version >/dev/null 2>&1; then + sed -i -e "$expr" "$file" + else + sed -i '' -e "$expr" "$file" + fi +} + enableFeatures() { local features="$1" - sed -i 's/experimental-features .*/& '"$features"'/' "$NIX_CONF_DIR"/nix.conf + sedInPlace 's/experimental-features .*/& '"$features"'/' "$NIX_CONF_DIR"/nix.conf } runinpty() { @@ -254,7 +265,7 @@ runinpty() { # Add a plugin to `plugin-files` configuration. loadPlugin() { local plugin_path="$1" - sed -i 's|plugin-files .*|& '"$plugin_path"'|' "$NIX_CONF_DIR"/nix.conf + sedInPlace 's|plugin-files .*|& '"$plugin_path"'|' "$NIX_CONF_DIR"/nix.conf } loadContribPlugin() { diff --git a/tests/functional/init.sh b/tests/functional/init.sh index 33ae8b2bd..aa0bac187 100755 --- a/tests/functional/init.sh +++ b/tests/functional/init.sh @@ -10,7 +10,7 @@ if test -d "$TEST_ROOT"; then killDaemon rm -rf "$TEST_ROOT" fi -mkdir "$TEST_ROOT" +mkdir -p "$TEST_ROOT" mkdir "$NIX_STORE_DIR" mkdir "$NIX_LOCALSTATE_DIR" diff --git a/tests/functional/meson.build b/tests/functional/meson.build index 75bbd2a87..3f25b5f08 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -156,6 +156,12 @@ if get_option('default_library') != 'static' functional_tests_scripts += ['plugins.sh'] endif +if get_option('default_library') != 'static' and get_option('enable-contrib-plugins') + functional_tests_scripts += [ + 'mtls-substituter-ssl-client-cert.sh' + ] +endif + # TODO(Qyriad): this will hopefully be able to be removed when we remove the autoconf+Make # buildsystem. See the comments at the top of setup-functional-tests.py for why this is here. meson.add_install_script( diff --git a/tests/functional/mtls-nix-binary-cache-ssl-server.py b/tests/functional/mtls-nix-binary-cache-ssl-server.py new file mode 100755 index 000000000..5bf4cbbba --- /dev/null +++ b/tests/functional/mtls-nix-binary-cache-ssl-server.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +import http.server +import ssl +import socketserver +import sys +import os +import argparse +import textwrap +from typing import Any + +class NixCacheHandler(http.server.BaseHTTPRequestHandler): + protocol_version: str = 'HTTP/1.1' + + def do_GET(self) -> None: + # Get client certificate information + try: + client_cert: dict[str, Any] | None = self.request.getpeercert() + except Exception as e: + print(f"Error getting client certificate: {e}", file=sys.stderr) + self.send_error(403, "Invalid client certificate") + return + + if not client_cert: + self.send_error(403, "No client certificate provided") + return + + # Additional validation - check if certificate chain is valid + subject: tuple[tuple[tuple[str, str], ...], ...] | None = client_cert.get('subject') + if not subject: + self.send_error(403, "Invalid client certificate: No subject") + return + + # Log client info + print(f"Client connected: {subject}", file=sys.stderr) + print(f"Path requested: {self.path}", file=sys.stderr) + + # Handle nix-cache-info endpoint + if self.path == '/nix-cache-info': + self.send_response(200) + self.send_header('Content-Type', 'text/plain') + self.send_header('Connection', 'close') # Explicitly close after response + test_root: str | None = os.environ.get('TEST_ROOT') + if not test_root: + store_root: str = '/nix/store' + else: + store_root = os.path.join(test_root, 'store') + + # Nix cache info format + cache_info: str = textwrap.dedent(f"""\ + StoreDir: {store_root} + WantMassQuery: 1 + Priority: 30 + """) + self.send_header('Content-Length', str(len(cache_info))) + self.end_headers() + self.wfile.write(cache_info.encode()) + self.wfile.flush() # Ensure data is sent + + # Handle .narinfo requests + elif self.path.endswith('.narinfo'): + # Return 404 for all narinfo requests (empty cache) + self.send_response(404) + self.send_header('Content-Length', '0') + self.send_header('Connection', 'close') + self.end_headers() + + else: + self.send_response(404) + self.send_header('Content-Length', '0') + self.send_header('Connection', 'close') + self.end_headers() + + def log_message(self, format: str, *args: Any) -> None: + # Suppress standard logging + pass + +def run_server(port_fifo_path: str, certfile: str, keyfile: str, ca_certfile: str) -> None: + # Create SSL context + context: ssl.SSLContext = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain(certfile=certfile, keyfile=keyfile) + context.verify_mode = ssl.VerifyMode.CERT_REQUIRED + context.check_hostname = False # We're not checking hostnames for client certs + context.load_verify_locations(cafile=ca_certfile) + + # Bind to a free port + with socketserver.TCPServer(("localhost", 0), NixCacheHandler) as httpd: + port = httpd.server_address[1] # Extract chosen port + + # Wrap with TLS + httpd.socket = context.wrap_socket(httpd.socket, server_side=True) + + # Write the port to the FIFO + with open(port_fifo_path, 'w') as fifo: + fifo.write(f"{port}\n") + fifo.flush() + + print(f"Server running on port {port}", file=sys.stderr) + + try: + httpd.serve_forever() + except KeyboardInterrupt: + httpd.shutdown() + +if __name__ == "__main__": + parser: argparse.ArgumentParser = argparse.ArgumentParser(description='Nix binary cache server with SSL client verification') + parser.add_argument('--port-fifo', type=str, required=True, help="FIFO where to inform about the port taken") + parser.add_argument('--cert', required=True, help='Server certificate file') + parser.add_argument('--key', required=True, help='Server private key file') + parser.add_argument('--ca-cert', required=True, help='CA certificate for client verification') + + args: argparse.Namespace = parser.parse_args() + + run_server(args.port_fifo, args.cert, args.key, args.ca_cert) diff --git a/tests/functional/mtls-substituter-ssl-client-cert.sh b/tests/functional/mtls-substituter-ssl-client-cert.sh new file mode 100755 index 000000000..721a3a500 --- /dev/null +++ b/tests/functional/mtls-substituter-ssl-client-cert.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash + +# shellcheck source=common.sh +source common.sh + +# Load the mTLS plugin for these tests. +loadContribPlugin "mtls_store" + +# Generate test certificates using EC keys for faster generation + +# Generate CA with EC key (with proper CA extensions for OpenSSL 3.x) +openssl ecparam -genkey -name prime256v1 -out "$TEST_ROOT/ca.key" 2>/dev/null +openssl req -new -x509 -days 1 -key "$TEST_ROOT/ca.key" -out "$TEST_ROOT/ca.crt" \ + -subj "/C=US/ST=Test/L=Test/O=TestCA/CN=Test CA" \ + -addext "basicConstraints=critical,CA:TRUE" \ + -addext "keyUsage=critical,keyCertSign,cRLSign" 2>/dev/null + +# Generate server certificate with EC key +openssl ecparam -genkey -name prime256v1 -out "$TEST_ROOT/server.key" 2>/dev/null +openssl req -new -key "$TEST_ROOT/server.key" -out "$TEST_ROOT/server.csr" \ + -subj "/C=US/ST=Test/L=Test/O=TestServer/CN=localhost" 2>/dev/null +openssl x509 -req -days 1 -in "$TEST_ROOT/server.csr" -CA "$TEST_ROOT/ca.crt" -CAkey "$TEST_ROOT/ca.key" \ + -sha256 -set_serial 01 -out "$TEST_ROOT/server.crt" 2>/dev/null + +# Generate client certificate with EC key +openssl ecparam -genkey -name prime256v1 -out "$TEST_ROOT/client.key" 2>/dev/null +openssl req -new -key "$TEST_ROOT/client.key" -out "$TEST_ROOT/client.csr" \ + -subj "/C=US/ST=Test/L=Test/O=TestClient/CN=Nix Test Client" 2>/dev/null +openssl x509 -req -days 1 -in "$TEST_ROOT/client.csr" -CA "$TEST_ROOT/ca.crt" -CAkey "$TEST_ROOT/ca.key" \ + -sha256 -set_serial 02 -out "$TEST_ROOT/client.crt" 2>/dev/null + +# Start the server and have it write its chosen port to the FIFO +FIFO_PATH="$TEST_ROOT/server-port.fifo" +mkfifo "$FIFO_PATH" +python3 "$PWD/mtls-nix-binary-cache-ssl-server.py" \ + --port-fifo "$FIFO_PATH" \ + --cert "$TEST_ROOT/server.crt" \ + --key "$TEST_ROOT/server.key" \ + --ca-cert "$TEST_ROOT/ca.crt" \ + > "$TEST_ROOT/server.log" 2>&1 & +SERVER_PID=$! + +# Function to stop server on exit +stopServer() { + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + rm -f "$FIFO_PATH" +} +trap stopServer EXIT + +# Read port from the FIFO (waits until server writes to it) but timeouts after 5s. +if ! PORT=$(timeout 5s bash -c "read -r line < '$FIFO_PATH'; echo \"\$line\""); then + echo "Timed out waiting for server to write port to FIFO" >&2 + exit 1 +fi + +if ! curl -sSf --cacert "$TEST_ROOT/ca.crt" --cert "$TEST_ROOT/client.crt" --key "$TEST_ROOT/client.key" \ + "https://localhost:$PORT/nix-cache-info" > /dev/null; then + if kill -0 "$SERVER_PID" 2>/dev/null; then + echo "Server started but did not respond to curl" >&2 + else + echo "Server failed to start" >&2 + fi + exit 1 +fi + +# Test 1: Verify server rejects connections without client certificate +echo "Testing connection without client certificate (should fail)..." >&2 +if curl -s --cacert "$TEST_ROOT/ca.crt" "https://localhost:$PORT/nix-cache-info" > /dev/null 2>&1; then + echo "FAIL: Server should have rejected connection without client cert" >&2 + exit 1 +fi + +# Test 2: Verify server accepts connections with client certificate +echo "Testing connection with client certificate..." >&2 +RESPONSE=$(curl -s --cacert "$TEST_ROOT/ca.crt" --cert "$TEST_ROOT/client.crt" --key "$TEST_ROOT/client.key" \ + "https://localhost:$PORT/nix-cache-info") + +if ! echo "$RESPONSE" | grepQuiet "StoreDir: "; then + echo "FAIL: Server should have accepted client certificate: $RESPONSE" >&2 + exit 1 +fi + +# Test 3: Test Nix with SSL client certificate parameters +# Set up substituter URL with SSL parameters +sslCache="https+mtls://localhost:$PORT?tls-certificate=$TEST_ROOT/client.crt&tls-private-key=$TEST_ROOT/client.key" + +# Configure Nix to trust our CA +export NIX_SSL_CERT_FILE="$TEST_ROOT/ca.crt" + +# Test nix store info +nix store ping --store "$sslCache" --json # | jq -e '.url' | grepQuiet "https://localhost:$PORT" + +# Test 4: Verify incorrect client certificate is rejected +# Generate a different client cert not signed by our CA (also using EC) +openssl ecparam -genkey -name prime256v1 -out "$TEST_ROOT/wrong.key" 2>/dev/null +openssl req -new -x509 -days 1 -key "$TEST_ROOT/wrong.key" -out "$TEST_ROOT/wrong.crt" \ + -subj "/C=US/ST=Test/L=Test/O=Wrong/CN=Wrong Client" 2>/dev/null + +wrongCache="https+mtls://localhost:$PORT?tls-certificate=$TEST_ROOT/wrong.crt&tls-private-key=$TEST_ROOT/wrong.key" + +rm -rf "$TEST_HOME" + +# This should fail +if nix store ping --download-attempts 0 --store "$wrongCache"; then + echo "FAIL: Should have rejected wrong certificate" >&2 + exit 1 +fi