contrib/plugins: init with mTLS store example
This is a collection of Lix plugins that showcase how to write one for various usecases. The first is a mTLS store plugin that enable mTLS cache URIs (`https+mtls://`). We enable meson build system support for this plugin but we are not going to distribute it in the official packaging of Lix, we will repackage each relevant plugin downstream in Nixpkgs. These plugins have *NO* guarantee support, they are provided as useful references and are possibly production-ready if your usecase is simple enough. Reference: https://github.com/NixOS/nix/pull/13030 (this change has resemblances but our APIs are different, the tests harness is mostly from CppNix). Change-Id: Ib354271981b35dff6c134b12c4748c3eaf743fcb Co-authored-by: Jörg Thalheim <joerg@thalheim.io> Co-authored-by: László Vaskó <1771332+vlaci@users.noreply.github.com> Signed-off-by: Raito Bezarius <raito@lix.systems>
This commit is contained in:
co-authored by
Jörg Thalheim
László Vaskó
parent
2d0c1e29d2
commit
569dcf9392
@@ -0,0 +1,14 @@
|
||||
plugin_mtls_store = shared_module(
|
||||
'plugin_mtls_store',
|
||||
'plugin_mtls_store.cc',
|
||||
dependencies : [
|
||||
liblixutil,
|
||||
liblixstore,
|
||||
liblixexpr,
|
||||
liblixfetchers,
|
||||
curl,
|
||||
],
|
||||
install : false,
|
||||
build_by_default : true,
|
||||
link_args : strict_shared_module_link_args,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
R"(
|
||||
|
||||
**Store URL format**: `https+mtls://...`
|
||||
|
||||
This store allows a binary cache to be accessed via the HTTPS
|
||||
protocol with mutual TLS mandated.
|
||||
|
||||
Two parameters can be passed to the query string:
|
||||
|
||||
- `tls-certificate`, a path to the TLS client certificate (optional)
|
||||
- `tls-private-key`, a path to the TLS private key backing the client certificate (required)
|
||||
|
||||
)"
|
||||
@@ -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 an optional TLS client certificate in PEM format as expected by CURLOPT_SSLCERT"
|
||||
};
|
||||
|
||||
PathsSetting<nix::Path> tlsKey{
|
||||
this,
|
||||
"",
|
||||
"tls-private-key",
|
||||
"Path of an 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);
|
||||
}
|
||||
|
||||
if (!keyring->tlsCertificate.empty()) {
|
||||
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>();
|
||||
}
|
||||
@@ -156,6 +156,9 @@ ma27:
|
||||
matthewbauer:
|
||||
github: matthewbauer
|
||||
|
||||
mic92:
|
||||
github: Mic92
|
||||
|
||||
midnightveil:
|
||||
display_name: julia
|
||||
forgejo: midnightveil
|
||||
@@ -243,6 +246,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]
|
||||
---
|
||||
|
||||
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.
|
||||
+9
-3
@@ -125,6 +125,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 = []
|
||||
|
||||
@@ -224,12 +225,13 @@ is_x64 = host_machine.cpu_family() == 'x86_64'
|
||||
# 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.
|
||||
shared_module_link_args = []
|
||||
# This is a stricter additional set of link flags.
|
||||
strict_shared_module_link_args = []
|
||||
if is_darwin
|
||||
shared_module_link_args += ['-undefined', 'suppress', '-flat_namespace']
|
||||
strict_shared_module_link_args += ['-flat_namespace']
|
||||
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 = { }
|
||||
|
||||
@@ -663,6 +665,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)
|
||||
|
||||
@@ -32,6 +32,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"
|
||||
|
||||
@@ -232,6 +232,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
./doc
|
||||
./lix
|
||||
./misc
|
||||
./contrib/plugins
|
||||
./COPYING
|
||||
]
|
||||
++ lib.optionals lintInsteadOfBuild [ ./.clang-tidy ]
|
||||
@@ -309,6 +310,10 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
jq
|
||||
yq
|
||||
lsof
|
||||
# For mTLS tests.
|
||||
curl
|
||||
openssl
|
||||
python3
|
||||
]
|
||||
++ lib.optional hostPlatform.isLinux util-linuxMinimal
|
||||
++ lib.optional (!officialRelease && buildUnreleasedNotes) build-release-notes
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -180,6 +180,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 += [
|
||||
'plugins/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(
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/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
|
||||
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" 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" \
|
||||
-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" \
|
||||
-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/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" &
|
||||
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 -k --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 -k "https://localhost:$PORT/nix-cache-info" 2>&1 | grep -q "certificate required"; then
|
||||
echo "FAIL: Server should have rejected connection" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Verify server accepts connections with client certificate
|
||||
echo "Testing connection with client certificate..." >&2
|
||||
RESPONSE=$(curl -v -s -k --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