From 36b0bebe253d87eef5a9a28318980fc586c41914 Mon Sep 17 00:00:00 2001 From: Damien Diederen Date: Wed, 17 May 2023 11:11:40 +0200 Subject: [PATCH 1/2] http-binary-cache-store: Add 'tls-certificate' and 'tls-private-key' settings Those are set via the store's URI, e.g.: https://substituter.invalid?tls-certificate=/path/to/cert.pem&tls-private-key=/path/to/key.pem --- doc/manual/rl-next/mtls-substituter.md | 15 +++++++++++++++ src/libstore/filetransfer.cc | 15 +++++++++++++++ src/libstore/http-binary-cache-store.cc | 18 +++++++++++++++++- src/libstore/include/nix/store/filetransfer.hh | 10 ++++++++++ .../nix/store/http-binary-cache-store.hh | 6 ++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 doc/manual/rl-next/mtls-substituter.md diff --git a/doc/manual/rl-next/mtls-substituter.md b/doc/manual/rl-next/mtls-substituter.md new file mode 100644 index 00000000000..a27c80e9fd1 --- /dev/null +++ b/doc/manual/rl-next/mtls-substituter.md @@ -0,0 +1,15 @@ +--- +synopsis: Support HTTPS binary caches using mTLS (client certificate) authentication +issues: [13002] +prs: [13030] +--- + +Added support for `tls-certificate` and `tls-private-key` options in substituter URLs. + +Example: + +``` +https://substituter.invalid?tls-certificate=/path/to/cert.pem&tls-private-key=/path/to/key.pem +``` + +When these options are configured, Nix will use this certificate/private key pair to authenticate to the server. diff --git a/src/libstore/filetransfer.cc b/src/libstore/filetransfer.cc index 72cdd5146ec..e32a0d62afe 100644 --- a/src/libstore/filetransfer.cc +++ b/src/libstore/filetransfer.cc @@ -531,6 +531,21 @@ struct curlFileTransfer : public FileTransfer if (writtenToSink) curl_easy_setopt(req, CURLOPT_RESUME_FROM_LARGE, writtenToSink); + /* Note that the underlying strings get copied by libcurl, so the path -> string conversion is ok: + > The application does not have to keep the string around after setting this option. + https://curl.se/libcurl/c/CURLOPT_SSLKEY.html + https://curl.se/libcurl/c/CURLOPT_SSLCERT.html */ + + if (request.tlsCert) { + curl_easy_setopt(req, CURLOPT_SSLCERTTYPE, "PEM"); + curl_easy_setopt(req, CURLOPT_SSLCERT, request.tlsCert->string().c_str()); + } + + if (request.tlsKey) { + curl_easy_setopt(req, CURLOPT_SSLKEYTYPE, "PEM"); + curl_easy_setopt(req, CURLOPT_SSLKEY, request.tlsKey->string().c_str()); + } + curl_easy_setopt(req, CURLOPT_ERRORBUFFER, errbuf); errbuf[0] = 0; diff --git a/src/libstore/http-binary-cache-store.cc b/src/libstore/http-binary-cache-store.cc index 92a551de880..943f1ed94d6 100644 --- a/src/libstore/http-binary-cache-store.cc +++ b/src/libstore/http-binary-cache-store.cc @@ -195,7 +195,23 @@ FileTransferRequest HttpBinaryCacheStore::makeRequest(std::string_view path) result.query = config->cacheUri.query; } - return FileTransferRequest(result); + FileTransferRequest request(result); + + /* Only use the specified SSL certificate and private key if the resolved URL names the same + authority and uses the same protocol. */ + if (result.scheme == config->cacheUri.scheme && result.authority == config->cacheUri.authority) { + if (const auto & cert = config->tlsCert.get()) { + debug("using TLS client certificate %s for '%s'", PathFmt(*cert), request.uri); + request.tlsCert = *cert; + } + + if (const auto & key = config->tlsKey.get()) { + debug("using TLS client key '%s' for '%s'", PathFmt(*key), request.uri); + request.tlsKey = *key; + } + } + + return request; } void HttpBinaryCacheStore::getFile(const std::string & path, Sink & sink) diff --git a/src/libstore/include/nix/store/filetransfer.hh b/src/libstore/include/nix/store/filetransfer.hh index 57b781c3320..500d9ebd7b4 100644 --- a/src/libstore/include/nix/store/filetransfer.hh +++ b/src/libstore/include/nix/store/filetransfer.hh @@ -121,6 +121,16 @@ struct FileTransferRequest ActivityId parentAct; bool decompress = true; + /** + * Optional path to the client certificate in "PEM" format. Only used for TLS-based protocols. + */ + std::optional tlsCert; + + /** + * Optional path to the client private key in "PEM" format. Only used for TLS-based protocols. + */ + std::optional tlsKey; + struct UploadData { UploadData(StringSource & s) diff --git a/src/libstore/include/nix/store/http-binary-cache-store.hh b/src/libstore/include/nix/store/http-binary-cache-store.hh index 12a21b27b08..9047233acf4 100644 --- a/src/libstore/include/nix/store/http-binary-cache-store.hh +++ b/src/libstore/include/nix/store/http-binary-cache-store.hh @@ -37,6 +37,12 @@ struct HttpBinaryCacheStoreConfig : std::enable_shared_from_this> tlsCert{ + this, std::nullopt, "tls-certificate", "Path to an optional TLS client certificate in PEM format."}; + + const Setting> tlsKey{ + this, std::nullopt, "tls-private-key", "Path to an optional TLS client certificate private key in PEM format."}; + static const std::string name() { return "HTTP Binary Cache Store"; From 0f17a1f655c719a15ce383b91fe9bf0910083748 Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Sun, 18 Jan 2026 20:30:18 +0300 Subject: [PATCH 2/2] libutil-tests: Add unit tests for https binary cache stores with mTLS This addresses the concerns with network isolation that have been raised previously [1] by only running the tests by default in a network namespace. This way all networks tests are independent of each other and do not bind to ports in the host namespace. This is much neater than doing these sorts of tests in functional suite. [1]: https://github.com/NixOS/nix/pull/14266#issuecomment-3411261285 --- src/libstore-test-support/https-store.cc | 125 ++++++++++++++++++ .../include/nix/store/tests/https-store.hh | 93 +++++++++++++ .../nix/store/tests/libstore-network.hh | 39 ++++++ .../include/nix/store/tests/meson.build | 2 + src/libstore-test-support/libstore-network.cc | 60 +++++++++ src/libstore-test-support/meson.build | 5 + src/libstore-test-support/package.nix | 2 + src/libstore-tests/http-binary-cache-store.cc | 87 ++++++++++++ src/libstore-tests/main.cc | 2 + src/libstore-tests/package.nix | 6 +- 10 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 src/libstore-test-support/https-store.cc create mode 100644 src/libstore-test-support/include/nix/store/tests/https-store.hh create mode 100644 src/libstore-test-support/include/nix/store/tests/libstore-network.hh create mode 100644 src/libstore-test-support/libstore-network.cc diff --git a/src/libstore-test-support/https-store.cc b/src/libstore-test-support/https-store.cc new file mode 100644 index 00000000000..bcf5ec17711 --- /dev/null +++ b/src/libstore-test-support/https-store.cc @@ -0,0 +1,125 @@ +#include "nix/store/tests/https-store.hh" + +#include + +namespace nix::testing { + +void TestHttpBinaryCacheStore::init() +{ + BinaryCacheStore::init(); +} + +ref TestHttpBinaryCacheStoreConfig::openTestStore() const +{ + auto store = make_ref( + ref{// FIXME we shouldn't actually need a mutable config + std::const_pointer_cast(shared_from_this())}); + store->init(); + return store; +} + +void HttpsBinaryCacheStoreTest::openssl(Strings args) +{ + runProgram("openssl", /*lookupPath=*/true, args); +} + +void HttpsBinaryCacheStoreTest::SetUp() +{ + LibStoreNetworkTest::SetUp(); + +#ifdef _WIN32 + GTEST_SKIP() << "HTTPS store tests are not supported on Windows"; +#endif + + tmpDir = createTempDir(); + cacheDir = tmpDir / "cache"; + delTmpDir = std::make_unique(tmpDir); + + localCacheStore = + make_ref("file", cacheDir.string(), LocalBinaryCacheStoreConfig::Params{}) + ->openStore(); + + caCert = tmpDir / "ca.crt"; + caKey = tmpDir / "ca.key"; + serverCert = tmpDir / "server.crt"; + serverKey = tmpDir / "server.key"; + clientCert = tmpDir / "client.crt"; + clientKey = tmpDir / "client.key"; + + // clang-format off + openssl({"ecparam", "-genkey", "-name", "prime256v1", "-out", caKey.string()}); + openssl({"req", "-new", "-x509", "-days", "1", "-key", caKey.string(), "-out", caCert.string(), "-subj", "/CN=TestCA"}); + openssl({"ecparam", "-genkey", "-name", "prime256v1", "-out", serverKey.string()}); + openssl({"req", "-new", "-key", serverKey.string(), "-out", (tmpDir / "server.csr").string(), "-subj", "/CN=localhost"}); + openssl({"x509", "-req", "-in", (tmpDir / "server.csr").string(), "-CA", caCert.string(), "-CAkey", caKey.string(), "-CAcreateserial", "-out", serverCert.string(), "-days", "1"}); + openssl({"ecparam", "-genkey", "-name", "prime256v1", "-out", clientKey.string()}); + openssl({"req", "-new", "-key", clientKey.string(), "-out", (tmpDir / "client.csr").string(), "-subj", "/CN=TestClient"}); + openssl({"x509", "-req", "-in", (tmpDir / "client.csr").string(), "-CA", caCert.string(), "-CAkey", caKey.string(), "-CAcreateserial", "-out", clientCert.string(), "-days", "1"}); + // clang-format on + +#ifndef _WIN32 /* FIXME: Can't yet start processes on windows */ + auto args = serverArgs(); + serverPid = startProcess( + [&] { + if (chdir(cacheDir.c_str()) == -1) + _exit(1); + std::vector argv; + argv.push_back(const_cast("openssl")); + for (auto & a : args) + argv.push_back(const_cast(a.c_str())); + argv.push_back(nullptr); + execvp("openssl", argv.data()); + _exit(1); + }, + {.dieWithParent = true}); +#endif + + /* As an optimization, sleep for a bit to allow the server to come up to avoid retrying when connecting. + This won't make the tests fail, but does make them run faster. We don't need to overcomplicate by waiting + for the port explicitly - this is enough. */ + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + /* FIXME: Don't use global settings. Tests are not run concurrently, so this is fine for now. */ + oldCaCert = settings.caFile; + settings.caFile = caCert.string(); +} + +void HttpsBinaryCacheStoreTest::TearDown() +{ + settings.caFile = oldCaCert; + serverPid.kill(); + delTmpDir.reset(); +} + +std::vector HttpsBinaryCacheStoreTest::serverArgs() +{ + return { + "s_server", + "-accept", + std::to_string(port), + "-cert", + serverCert.string(), + "-key", + serverKey.string(), + "-WWW", /* Serve from current directory. */ + "-quiet", + }; +} + +std::vector HttpsBinaryCacheStoreMtlsTest::serverArgs() +{ + auto args = HttpsBinaryCacheStoreTest::serverArgs(); + /* With the -Verify option the client must supply a certificate or an error occurs, which is not the + case with -verify. */ + args.insert(args.end(), {"-CAfile", caCert.string(), "-Verify", "1", "-verify_return_error"}); + return args; +} + +ref HttpsBinaryCacheStoreTest::makeConfig(BinaryCacheStoreConfig::Params params) +{ + auto res = make_ref("https", fmt("localhost:%d", port), std::move(params)); + res->pathInfoCacheSize = 0; /* We don't want any caching in tests. */ + return res; +} + +} // namespace nix::testing diff --git a/src/libstore-test-support/include/nix/store/tests/https-store.hh b/src/libstore-test-support/include/nix/store/tests/https-store.hh new file mode 100644 index 00000000000..17b38e7d51c --- /dev/null +++ b/src/libstore-test-support/include/nix/store/tests/https-store.hh @@ -0,0 +1,93 @@ +#pragma once +///@file + +#include +#include + +#include "nix/store/tests/libstore-network.hh" +#include "nix/store/http-binary-cache-store.hh" +#include "nix/store/store-api.hh" +#include "nix/store/globals.hh" +#include "nix/store/local-binary-cache-store.hh" +#include "nix/util/file-system.hh" +#include "nix/util/processes.hh" + +namespace nix::testing { + +class TestHttpBinaryCacheStoreConfig; + +/** + * Test shim for testing. We don't want to use the on-disk narinfo cache in unit + * tests. + */ +class TestHttpBinaryCacheStore : public HttpBinaryCacheStore +{ +public: + TestHttpBinaryCacheStore(const TestHttpBinaryCacheStore &) = delete; + TestHttpBinaryCacheStore(TestHttpBinaryCacheStore &&) = delete; + TestHttpBinaryCacheStore & operator=(const TestHttpBinaryCacheStore &) = delete; + TestHttpBinaryCacheStore & operator=(TestHttpBinaryCacheStore &&) = delete; + + TestHttpBinaryCacheStore(ref config) + : Store{*config} + , BinaryCacheStore{*config} + , HttpBinaryCacheStore(config) + { + diskCache = nullptr; /* Disable caching, we'll be creating a new binary cache for each test. */ + } + + void init() override; +}; + +class TestHttpBinaryCacheStoreConfig : public HttpBinaryCacheStoreConfig +{ +public: + TestHttpBinaryCacheStoreConfig( + std::string_view scheme, std::string_view cacheUri, const Store::Config::Params & params) + : StoreConfig(params) + , HttpBinaryCacheStoreConfig(scheme, cacheUri, params) + { + } + + ref openTestStore() const; + + ref openStore() const override + { + return openTestStore(); + } +}; + +class HttpsBinaryCacheStoreTest : public virtual LibStoreNetworkTest +{ + std::unique_ptr delTmpDir; + +public: + static void SetUpTestSuite() + { + initLibStore(/*loadConfig=*/false); + } + +protected: + std::filesystem::path tmpDir, cacheDir; + std::filesystem::path caCert, caKey, serverCert, serverKey; + std::filesystem::path clientCert, clientKey; + std::string oldCaCert; + Pid serverPid; + uint16_t port = 8443; + std::shared_ptr localCacheStore; + + static void openssl(Strings args); + void SetUp() override; + void TearDown() override; + + virtual std::vector serverArgs(); + ref makeConfig(BinaryCacheStoreConfig::Params params); +}; + +class HttpsBinaryCacheStoreMtlsTest : public HttpsBinaryCacheStoreTest +{ +protected: + std::vector serverArgs() override; +}; + +} // namespace nix::testing diff --git a/src/libstore-test-support/include/nix/store/tests/libstore-network.hh b/src/libstore-test-support/include/nix/store/tests/libstore-network.hh new file mode 100644 index 00000000000..ab03ace5618 --- /dev/null +++ b/src/libstore-test-support/include/nix/store/tests/libstore-network.hh @@ -0,0 +1,39 @@ +#pragma once +/// @file + +#include + +namespace nix::testing { + +/** + * Whether to run network tests. This is global so that the test harness can + * enable this by default if we can run tests in isolation. + */ +extern bool networkTestsAvailable; + +/** + * Set up network tests and, if on linux, create a new network namespace for + * tests with a loopback interface. This is to avoid binding to ports in the + * host's namespace. + */ +void setupNetworkTests(); + +class LibStoreNetworkTest : public virtual ::testing::Test +{ +protected: + void SetUp() override + { + if (networkTestsAvailable) + return; + static bool warned = false; + if (!warned) { + warned = true; + GTEST_SKIP() + << "Network tests not enabled by default without user namespaces, use NIX_TEST_FORCE_NETWORK_TESTS=1 to override"; + } else { + GTEST_SKIP(); + } + } +}; + +} // namespace nix::testing diff --git a/src/libstore-test-support/include/nix/store/tests/meson.build b/src/libstore-test-support/include/nix/store/tests/meson.build index 33524de3851..8d844d24e7d 100644 --- a/src/libstore-test-support/include/nix/store/tests/meson.build +++ b/src/libstore-test-support/include/nix/store/tests/meson.build @@ -4,6 +4,8 @@ include_dirs = [ include_directories('../../..') ] headers = files( 'derived-path.hh', + 'https-store.hh', + 'libstore-network.hh', 'libstore.hh', 'nix_api_store.hh', 'outputs-spec.hh', diff --git a/src/libstore-test-support/libstore-network.cc b/src/libstore-test-support/libstore-network.cc new file mode 100644 index 00000000000..8aa047bdd60 --- /dev/null +++ b/src/libstore-test-support/libstore-network.cc @@ -0,0 +1,60 @@ +#include "nix/store/tests/libstore-network.hh" +#include "nix/util/error.hh" +#include "nix/util/environment-variables.hh" + +#ifdef __linux__ +# include "nix/util/file-system.hh" +# include "nix/util/linux-namespaces.hh" +# include +# include +# include +# include +#endif + +namespace nix::testing { + +bool networkTestsAvailable = false; + +#ifdef __linux__ + +static void enterNetworkNamespace() +{ + auto uid = ::getuid(); + auto gid = ::getgid(); + + if (::unshare(CLONE_NEWUSER | CLONE_NEWNET) == -1) + throw SysError("setting up a private network namespace for tests"); + + std::filesystem::path procSelf = "/proc/self"; + writeFile(procSelf / "setgroups", "deny"); + writeFile(procSelf / "uid_map", fmt("%d %d 1", uid, uid)); + writeFile(procSelf / "gid_map", fmt("%d %d 1", gid, gid)); + + AutoCloseFD fd(::socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); + if (!fd) + throw SysError("cannot open IP socket for loopback interface"); + + struct ::ifreq ifr = {}; + strcpy(ifr.ifr_name, "lo"); + ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; + if (::ioctl(fd.get(), SIOCSIFFLAGS, &ifr) == -1) + throw SysError("cannot set loopback interface flags"); +} + +#endif + +void setupNetworkTests() +try { + networkTestsAvailable = getEnvOs(OS_STR("NIX_TEST_FORCE_NETWORK_TESTS")).has_value(); + +#ifdef __linux__ + if (!networkTestsAvailable && userNamespacesSupported()) { + enterNetworkNamespace(); + networkTestsAvailable = true; + } +#endif +} catch (SystemError & e) { + /* Ignore any set up errors. */ +} + +} // namespace nix::testing diff --git a/src/libstore-test-support/meson.build b/src/libstore-test-support/meson.build index 8617225d743..4d904cb1d06 100644 --- a/src/libstore-test-support/meson.build +++ b/src/libstore-test-support/meson.build @@ -28,10 +28,15 @@ subdir('nix-meson-build-support/subprojects') rapidcheck = dependency('rapidcheck') deps_public += rapidcheck +gtest = dependency('gtest') +deps_public += gtest + subdir('nix-meson-build-support/common') sources = files( 'derived-path.cc', + 'https-store.cc', + 'libstore-network.cc', 'outputs-spec.cc', 'path.cc', 'test-main.cc', diff --git a/src/libstore-test-support/package.nix b/src/libstore-test-support/package.nix index 391ddeefda2..4f20f2cbe71 100644 --- a/src/libstore-test-support/package.nix +++ b/src/libstore-test-support/package.nix @@ -7,6 +7,7 @@ nix-store-c, rapidcheck, + gtest, # Configuration Options @@ -39,6 +40,7 @@ mkMesonLibrary (finalAttrs: { nix-store nix-store-c rapidcheck + gtest ]; mesonFlags = [ diff --git a/src/libstore-tests/http-binary-cache-store.cc b/src/libstore-tests/http-binary-cache-store.cc index 4b3754a1fe4..bee23216f57 100644 --- a/src/libstore-tests/http-binary-cache-store.cc +++ b/src/libstore-tests/http-binary-cache-store.cc @@ -1,6 +1,9 @@ #include +#include #include "nix/store/http-binary-cache-store.hh" +#include "nix/store/tests/https-store.hh" +#include "nix/util/fs-sink.hh" namespace nix { @@ -34,4 +37,88 @@ TEST(HttpBinaryCacheStore, constructConfigWithParamsAndUrlWithParams) EXPECT_EQ(config.getReference().params, params); } +using testing::HttpsBinaryCacheStoreMtlsTest; +using testing::HttpsBinaryCacheStoreTest; + +using namespace std::string_view_literals; +using namespace std::string_literals; + +TEST_F(HttpsBinaryCacheStoreTest, queryPathInfo) +{ + auto config = makeConfig({}); + auto store = config->openStore(); + StringSource dump{"test"sv}; + auto path = localCacheStore->addToStoreFromDump(dump, "test-name", FileSerialisationMethod::Flat); + EXPECT_NO_THROW(store->queryPathInfo(path)); +} + +auto withNoRetries() +{ + auto oldTries = fileTransferSettings.tries.get(); + Finally restoreTries = [=]() { fileTransferSettings.tries = oldTries; }; + fileTransferSettings.tries = 1; /* FIXME: Don't use global settings. */ + return restoreTries; +} + +TEST_F(HttpsBinaryCacheStoreMtlsTest, queryPathInfo) +{ + auto config = makeConfig({ + {"tls-certificate"s, clientCert.string()}, + {"tls-private-key"s, clientKey.string()}, + }); + auto store = config->openStore(); + StringSource dump{"test"sv}; + auto path = localCacheStore->addToStoreFromDump(dump, "test-name", FileSerialisationMethod::Flat); + EXPECT_NO_THROW(store->queryPathInfo(path)); +} + +TEST_F(HttpsBinaryCacheStoreMtlsTest, rejectsWithoutClientCert) +{ + auto restoreTries = withNoRetries(); + auto config = makeConfig({}); + EXPECT_THROW(config->openStore(), Error); +} + +TEST_F(HttpsBinaryCacheStoreMtlsTest, rejectsWrongClientCert) +{ + auto wrongKey = tmpDir / "wrong.key"; + auto wrongCert = tmpDir / "wrong.crt"; + + // clang-format off + openssl({"ecparam", "-genkey", "-name", "prime256v1", "-out", wrongKey.string()}); + openssl({"req", "-new", "-x509", "-days", "1", "-key", wrongKey.string(), "-out", wrongCert.string(), "-subj", "/CN=WrongClient"}); + // clang-format on + + auto config = makeConfig({ + {"tls-certificate"s, wrongCert.string()}, + {"tls-private-key"s, wrongKey.string()}, + }); + auto restoreTries = withNoRetries(); + EXPECT_THROW(config->openStore(), Error); +} + +TEST_F(HttpsBinaryCacheStoreMtlsTest, doesNotSendCertOnRedirectToDifferentAuthority) +{ + StringSource dump{"test"sv}; + auto path = localCacheStore->addToStoreFromDump(dump, "test-name", FileSerialisationMethod::Flat); + + for (auto & entry : DirectoryIterator{cacheDir}) + if (entry.path().extension() == ".narinfo") { + auto content = readFile(entry.path()); + content = std::regex_replace(content, std::regex("URL: nar/"), fmt("URL: https://127.0.0.1:%d/nar/", port)); + writeFile(entry.path(), content); + } + + auto config = makeConfig({ + {"tls-certificate"s, clientCert.string()}, + {"tls-private-key"s, clientKey.string()}, + }); + auto store = config->openStore(); + + auto restoreTries = withNoRetries(); + auto info = store->queryPathInfo(path); + NullSink null; + EXPECT_THROW(store->narFromPath(path, null), Error); +} + } // namespace nix diff --git a/src/libstore-tests/main.cc b/src/libstore-tests/main.cc index ffe9816134f..c45e3a7f384 100644 --- a/src/libstore-tests/main.cc +++ b/src/libstore-tests/main.cc @@ -1,6 +1,7 @@ #include #include "nix/store/tests/test-main.hh" +#include "nix/store/tests/libstore-network.hh" using namespace nix; @@ -10,6 +11,7 @@ int main(int argc, char ** argv) if (res) return res; + nix::testing::setupNetworkTests(); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/src/libstore-tests/package.nix b/src/libstore-tests/package.nix index ac547aca35e..2bd0ab1a5db 100644 --- a/src/libstore-tests/package.nix +++ b/src/libstore-tests/package.nix @@ -9,6 +9,7 @@ nix-store-c, nix-store-test-support, sqlite, + openssl, rapidcheck, gtest, @@ -75,7 +76,10 @@ mkMesonExecutable (finalAttrs: { runCommand "${finalAttrs.pname}-run" { meta.broken = !stdenv.hostPlatform.emulatorAvailable buildPackages; - buildInputs = [ writableTmpDirAsHomeHook ]; + nativeBuildInputs = [ + writableTmpDirAsHomeHook + openssl + ]; } ( ''