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
This commit is contained in:
committed by
Niko Klanecek
parent
7068cbf010
commit
728d2bfee7
@@ -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,
|
||||
)
|
||||
@@ -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.
|
||||
|
||||
)"
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "lix/libstore/store-api.hh"
|
||||
#include "lix/libutil/config.hh"
|
||||
#include "lix/libstore/http-binary-cache-store.hh"
|
||||
#include <stdlib.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
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<nix::Path> tlsCertificate{
|
||||
this,
|
||||
"",
|
||||
"tls-certificate",
|
||||
"Path of the TLS client certificate in PEM format as expected by CURLOPT_SSLCERT"
|
||||
};
|
||||
|
||||
PathsSetting<nix::Path> 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> 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<Keyring>(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<std::string> uriSchemes()
|
||||
{
|
||||
return {"https+mtls"};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
extern "C" void nix_plugin_entry()
|
||||
{
|
||||
nix::StoreImplementations::add<nix::mTLSBinaryCacheStoreImpl, nix::mTLSBinaryCacheStoreConfig>();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
+14
-4
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
+113
@@ -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)
|
||||
+108
@@ -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
|
||||
Reference in New Issue
Block a user