Compare commits

...
Author SHA1 Message Date
Raito Bezarius de0447c2c6 -------------- ongoing crimes
Change-Id: Iedd51025b3a5a25b6570983726f8a237c53f5192
2025-11-21 17:40:21 +01:00
Raito Bezarius da75fb29e7 contrib/plugins: init with mTLS store example
This reverts commit 2b42901ec7.
Reason for revert: the work required has been achieved.

Change-Id: I917b2f87e436a4e34bec0b788634d949b0236c62
2025-11-18 20:13:46 +01:00
55 changed files with 844 additions and 117 deletions
+14
View File
@@ -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)
)"
+102
View File
@@ -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>();
}
+3
View File
@@ -258,6 +258,9 @@ vigress8:
forgejo: vigress8 forgejo: vigress8
github: vigress8 github: vigress8
vlaci:
github: vlaci
vlinkz: vlinkz:
display_name: Victor Fuentes display_name: Victor Fuentes
forgejo: vlinkz forgejo: vlinkz
+42
View File
@@ -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.
+1 -1
View File
@@ -3,7 +3,7 @@
#include "lix/libcmd/built-path.hh" #include "lix/libcmd/built-path.hh"
#include "lix/libexpr/eval.hh" #include "lix/libexpr/eval.hh"
#include "lix/libexpr/flake/flakeref.hh" #include "lix/libflakes/expr/flakeref.hh"
#include "lix/libexpr/get-drvs.hh" #include "lix/libexpr/get-drvs.hh"
#include "lix/libutil/types.hh" #include "lix/libutil/types.hh"
#include "lix/libutil/json-fwd.hh" #include "lix/libutil/json-fwd.hh"
+3
View File
@@ -1,4 +1,5 @@
#include "lix/libcmd/command.hh" #include "lix/libcmd/command.hh"
#include "lix/libflakes/init.hh"
#include "lix/libstore/store-api.hh" #include "lix/libstore/store-api.hh"
#include "lix/libstore/local-fs-store.hh" #include "lix/libstore/local-fs-store.hh"
#include "lix/libstore/derivations.hh" #include "lix/libstore/derivations.hh"
@@ -119,6 +120,8 @@ ref<eval_cache::CachingEvaluator> EvalCommand::getEvaluator()
startReplOnEvalErrors ? AbstractNixRepl::runSimple : nullptr startReplOnEvalErrors ? AbstractNixRepl::runSimple : nullptr
); );
flake::initFlakes(evalState.get());
evalState->repair = repair; evalState->repair = repair;
} }
return ref<eval_cache::CachingEvaluator>::unsafeFromPtr(evalState); return ref<eval_cache::CachingEvaluator>::unsafeFromPtr(evalState);
+1 -1
View File
@@ -6,7 +6,7 @@
#include "lix/libutil/args.hh" #include "lix/libutil/args.hh"
#include "lix/libcmd/common-eval-args.hh" #include "lix/libcmd/common-eval-args.hh"
#include "lix/libstore/path.hh" #include "lix/libstore/path.hh"
#include "lix/libexpr/flake/lockfile.hh" #include "lix/libflakes/expr/lockfile.hh"
#include "lix/libutil/async.hh" #include "lix/libutil/async.hh"
#include <optional> #include <optional>
+1 -1
View File
@@ -5,7 +5,7 @@
#include "lix/libexpr/eval.hh" #include "lix/libexpr/eval.hh"
#include "lix/libfetchers/fetchers.hh" #include "lix/libfetchers/fetchers.hh"
#include "lix/libfetchers/registry.hh" #include "lix/libfetchers/registry.hh"
#include "lix/libexpr/flake/flakeref.hh" #include "lix/libflakes/expr/flakeref.hh"
#include "lix/libstore/store-api.hh" #include "lix/libstore/store-api.hh"
#include "lix/libcmd/command.hh" #include "lix/libcmd/command.hh"
#include "lix/libutil/async.hh" #include "lix/libutil/async.hh"
+1 -1
View File
@@ -5,7 +5,7 @@
#include "lix/libcmd/common-eval-args.hh" #include "lix/libcmd/common-eval-args.hh"
#include "lix/libexpr/eval.hh" #include "lix/libexpr/eval.hh"
#include "lix/libexpr/get-drvs.hh" #include "lix/libexpr/get-drvs.hh"
#include "lix/libexpr/flake/flake.hh" #include "lix/libflakes/expr/flake.hh"
namespace nix { namespace nix {
+1 -1
View File
@@ -4,7 +4,7 @@
#include "lix/libexpr/attr-path.hh" #include "lix/libexpr/attr-path.hh"
#include "lix/libcmd/common-eval-args.hh" #include "lix/libcmd/common-eval-args.hh"
#include "lix/libexpr/eval.hh" #include "lix/libexpr/eval.hh"
#include "lix/libexpr/flake/flake.hh" #include "lix/libflakes/expr/flake.hh"
#include "lix/libexpr/eval-cache.hh" #include "lix/libexpr/eval-cache.hh"
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
+1 -1
View File
@@ -4,7 +4,7 @@
#include "lix/libcmd/installables.hh" #include "lix/libcmd/installables.hh"
#include "lix/libexpr/eval-cache.hh" #include "lix/libexpr/eval-cache.hh"
#include "lix/libexpr/eval.hh" #include "lix/libexpr/eval.hh"
#include "lix/libexpr/flake/flake.hh" #include "lix/libflakes/expr/flake.hh"
namespace nix { namespace nix {
+1 -1
View File
@@ -14,7 +14,7 @@
#include "lix/libexpr/eval-settings.hh" #include "lix/libexpr/eval-settings.hh"
#include "lix/libstore/store-api.hh" #include "lix/libstore/store-api.hh"
#include "lix/libmain/shared.hh" #include "lix/libmain/shared.hh"
#include "lix/libexpr/flake/flake.hh" #include "lix/libflakes/expr/flake.hh"
#include "lix/libexpr/eval-cache.hh" #include "lix/libexpr/eval-cache.hh"
#include "lix/libfetchers/registry.hh" #include "lix/libfetchers/registry.hh"
#include "lix/libstore/build-result.hh" #include "lix/libstore/build-result.hh"
+2
View File
@@ -45,6 +45,7 @@ libcmd = library(
liblixstore, liblixstore,
liblixfetchers, liblixfetchers,
liblixexpr, liblixexpr,
liblixflakes,
liblixmain, liblixmain,
liblix_doc, liblix_doc,
boehm, boehm,
@@ -76,6 +77,7 @@ liblixcmd = declare_dependency(
dependencies : [ dependencies : [
liblixutil, liblixutil,
liblixstore, liblixstore,
liblixflakes,
kj, kj,
], ],
link_with : libcmd, link_with : libcmd,
+2 -2
View File
@@ -22,8 +22,8 @@
#include "lix/libexpr/get-drvs.hh" #include "lix/libexpr/get-drvs.hh"
#include "lix/libstore/derivations.hh" #include "lix/libstore/derivations.hh"
#include "lix/libstore/globals.hh" #include "lix/libstore/globals.hh"
#include "lix/libexpr/flake/flake.hh" #include "lix/libflakes/expr/flake.hh"
#include "lix/libexpr/flake/lockfile.hh" #include "lix/libflakes/expr/lockfile.hh"
#include "lix/libcmd/editor-for.hh" #include "lix/libcmd/editor-for.hh"
#include "lix/libutil/finally.hh" #include "lix/libutil/finally.hh"
#include "lix/libcmd/markdown.hh" #include "lix/libcmd/markdown.hh"
+1 -2
View File
@@ -6,8 +6,7 @@ renameInGlobalScope: false
Load, parse and return the Nix expression in the file *path*. Load, parse and return the Nix expression in the file *path*.
The value *path* can be a path, a string, or an attribute set with an The value *path* can be a path, a string, or an attribute set with an
`__toString` attribute or a `outPath` attribute (as derivations or flake `__toString` attribute or a `outPath` attribute (as derivations typically have).
inputs typically have).
If *path* is a directory, the file `default.nix` in that directory If *path* is a directory, the file `default.nix` in that directory
is loaded. is loaded.
+3 -39
View File
@@ -6,48 +6,12 @@
namespace nix { namespace nix {
/* Very hacky way to parse $NIX_PATH, which is colon-separated, but
can contain URLs (e.g. "nixpkgs=https://bla...:foo=https://"). */
static Strings parseNixPath(const std::string & s)
{
Strings res;
auto p = s.begin();
while (p != s.end()) {
auto start = p;
auto start2 = p;
while (p != s.end() && *p != ':') {
if (*p == '=') start2 = p + 1;
++p;
}
if (p == s.end()) {
if (p != start) res.push_back(std::string(start, p));
break;
}
if (*p == ':') {
auto prefix = std::string(start2, s.end());
if (EvalSettings::isPseudoUrl(prefix) || prefix.starts_with("flake:")) {
++p;
while (p != s.end() && *p != ':') ++p;
}
res.push_back(std::string(start, p));
if (p == s.end()) break;
}
++p;
}
return res;
}
EvalSettings::EvalSettings() EvalSettings::EvalSettings()
{ {
auto var = getEnv("NIX_PATH"); //auto var = getEnv("NIX_PATH");
if (var) nixPath.setDefault(parseNixPath(*var)); // TODO(flakes): move that allowed prefix and parse it from eval plugins.
//if (var) nixPath.setDefault(parseNixPath(*var, ));
} }
Strings EvalSettings::getDefaultNixPath() Strings EvalSettings::getDefaultNixPath()
+83 -25
View File
@@ -21,7 +21,7 @@
#include "lix/libexpr/print.hh" #include "lix/libexpr/print.hh"
#include "lix/libexpr/gc-small-vector.hh" #include "lix/libexpr/gc-small-vector.hh"
#include "lix/libfetchers/fetch-to-store.hh" #include "lix/libfetchers/fetch-to-store.hh"
#include "lix/libexpr/flake/flakeref.hh" #include "lix/libfetchers/fetchers.hh"
#include "lix/libutil/exit.hh" #include "lix/libutil/exit.hh"
#include "lix/libutil/json.hh" #include "lix/libutil/json.hh"
#include "symbol-table.hh" #include "symbol-table.hh"
@@ -318,14 +318,16 @@ EvalBuiltins::EvalBuiltins(
EvalPaths::EvalPaths( EvalPaths::EvalPaths(
AsyncIoRoot & aio, AsyncIoRoot & aio,
const ref<Store> & store, const ref<Store> & store,
SearchPath searchPath, SearchPath initialSearchPath,
bool pureEval,
EvalErrorContext & errors EvalErrorContext & errors
) )
: store(store) : store(store)
, searchPath_(std::move(searchPath)) , searchPath_(std::move(initialSearchPath))
, pureEval(pureEval)
, errors(errors) , errors(errors)
{ {
if (evalSettings.restrictEval || evalSettings.pureEval) { if (evalSettings.restrictEval || pureEval) {
allowedPaths = AllowedPath{.allowAllChildren = false}; allowedPaths = AllowedPath{.allowAllChildren = false};
for (auto & i : searchPath_.elements) { for (auto & i : searchPath_.elements) {
@@ -349,6 +351,70 @@ EvalPaths::EvalPaths(
} }
} }
/* Very hacky way to parse $NIX_PATH, which is colon-separated, but
can contain URLs (e.g. "nixpkgs=https://bla...:foo=https://"). */
static Strings parseNixPath(const std::string & s, const std::set<std::string> & allowedPrefixes)
{
Strings res;
auto p = s.begin();
while (p != s.end()) {
auto start = p;
auto start2 = p;
while (p != s.end() && *p != ':') {
if (*p == '=') start2 = p + 1;
++p;
}
if (p == s.end()) {
if (p != start) res.push_back(std::string(start, p));
break;
}
if (*p == ':') {
auto prefix = std::string(start2, s.end());
auto matchesAllowedPrefixes = std::any_of(
allowedPrefixes.begin(),
allowedPrefixes.end(),
[&](const std::string & allowedPrefix) {
return prefix.starts_with(allowedPrefix);
}
);
if (EvalSettings::isPseudoUrl(prefix) || matchesAllowedPrefixes) {
++p;
while (p != s.end() && *p != ':') ++p;
}
res.push_back(std::string(start, p));
if (p == s.end()) break;
}
++p;
}
return res;
}
void EvalPaths::parseAndConfigureEnvironmentNixPath(std::string const & value)
{
if (!pureEval) {
for (auto & elem : parseNixPath(value, allowedNixPathPrefixes)) {
searchPath_.elements.emplace_back(SearchPath::Elem::parse(elem));
}
}
}
void EvalPaths::configureSettingNixPath(std::list<std::string> const & values)
{
if (!pureEval) {
for (auto & elem : values) {
searchPath_.elements.emplace_back(SearchPath::Elem::parse(elem));
}
}
}
Evaluator::Evaluator( Evaluator::Evaluator(
AsyncIoRoot & aio, AsyncIoRoot & aio,
const SearchPath & _searchPath, const SearchPath & _searchPath,
@@ -357,16 +423,7 @@ Evaluator::Evaluator(
std::function<ReplExitStatus(EvalState & es, ValMap const & extraEnv)> debugRepl std::function<ReplExitStatus(EvalState & es, ValMap const & extraEnv)> debugRepl
) )
: s(symbols) : s(symbols)
, paths(aio, store, [&] { , paths(aio, store, _searchPath, evalSettings.pureEval, errors)
SearchPath searchPath;
if (!evalSettings.pureEval) {
for (auto & i : _searchPath.elements)
searchPath.elements.emplace_back(SearchPath::Elem {i});
for (auto & i : evalSettings.nixPath.get())
searchPath.elements.emplace_back(SearchPath::Elem::parse(i));
}
return searchPath;
}(), errors)
, builtins(mem, symbols, paths.searchPath(), store->config().storeDir) , builtins(mem, symbols, paths.searchPath(), store->config().storeDir)
, repair(NoRepair) , repair(NoRepair)
, store(store) , store(store)
@@ -392,6 +449,11 @@ Evaluator::Evaluator(
box_ptr<EvalState> Evaluator::begin(AsyncIoRoot & aio) box_ptr<EvalState> Evaluator::begin(AsyncIoRoot & aio)
{ {
assert(!activeEval); assert(!activeEval);
if (auto envNixPath = getEnvNonEmpty("NIX_PATH")) {
paths.parseAndConfigureEnvironmentNixPath(*envNixPath);
} else {
paths.configureSettingNixPath(evalSettings.nixPath);
}
return box_ptr<EvalState>::unsafeFromNonnull( return box_ptr<EvalState>::unsafeFromNonnull(
std::unique_ptr<EvalState>(new EvalState(aio, *this)) std::unique_ptr<EvalState>(new EvalState(aio, *this))
); );
@@ -2972,7 +3034,12 @@ try {
std::optional<std::string> res; std::optional<std::string> res;
if (EvalSettings::isPseudoUrl(value)) { for (auto & responder : searchPathResponders) {
if (responder.canResolve(value))
res = TRY_AWAIT(responder.resolve(value, store));
}
if (!res && EvalSettings::isPseudoUrl(value)) {
try { try {
auto storePath = TRY_AWAIT(fetchers::downloadTarball( auto storePath = TRY_AWAIT(fetchers::downloadTarball(
store, EvalSettings::resolvePseudoUrl(value), "source", false)).tree.storePath; store, EvalSettings::resolvePseudoUrl(value), "source", false)).tree.storePath;
@@ -2984,16 +3051,7 @@ try {
} }
} }
else if (value.starts_with("flake:")) { else if (!res) {
experimentalFeatureSettings.require(Xp::Flakes);
auto flakeRef = parseFlakeRef(value.substr(6), {}, true, false);
debug("fetching flake search path element '%s''", value);
auto storePath =
TRY_AWAIT(TRY_AWAIT(flakeRef.resolve(store)).fetchTree(store)).first.storePath;
res = {store->toRealPath(storePath)};
}
else {
auto path = absPath(value); auto path = absPath(value);
if (pathExists(path)) if (pathExists(path))
res = { path }; res = { path };
+28 -1
View File
@@ -320,20 +320,47 @@ struct EvalErrorContext
class EvalPaths class EvalPaths
{ {
struct Responder {
std::function<bool (std::string const &)> canResolve;
std::function<kj::Promise<Result<std::optional<std::string>>> (std::string const &, ref<Store>)> resolve;
};
ref<Store> store; ref<Store> store;
std::set<std::string> allowedNixPathPrefixes;
std::vector<Responder> searchPathResponders;
SearchPath searchPath_; SearchPath searchPath_;
bool pureEval;
EvalErrorContext & errors; EvalErrorContext & errors;
public: public:
EvalPaths( EvalPaths(
AsyncIoRoot & aio, AsyncIoRoot & aio,
const ref<Store> & store, const ref<Store> & store,
SearchPath searchPath, SearchPath initialSearchPath,
bool pureEval,
EvalErrorContext & errors EvalErrorContext & errors
); );
void parseAndConfigureEnvironmentNixPath(std::string const & value);
void configureSettingNixPath(std::list<std::string> const & value);
const SearchPath & searchPath() const { return searchPath_; } const SearchPath & searchPath() const { return searchPath_; }
std::string resolvePseudoUrl(std::string_view url) {
if (url.starts_with("channel:"))
return "https://channels.nixos.org/" + std::string(url.substr(8)) + "/nixexprs.tar.xz";
else
return std::string(url);
}
bool registerAllowedNixPathPrefix(const std::string & allowedPrefix) {
auto [_, added] = allowedNixPathPrefixes.insert(allowedPrefix);
return added;
}
void registerSearchPathResolver(std::function<bool (std::string const &)> eligible, std::function<kj::Promise<Result<std::optional<std::string>>> (std::string const & value, ref<Store> store)> resolver) {
searchPathResponders.push_back({.canResolve=eligible,.resolve=resolver});
}
private: private:
struct AllowedPath struct AllowedPath
{ {
-8
View File
@@ -19,12 +19,4 @@ void prim_getContext(EvalState & state, Value * * args, Value & v);
void prim_hasContext(EvalState & state, Value * * args, Value & v); void prim_hasContext(EvalState & state, Value * * args, Value & v);
void prim_unsafeDiscardOutputDependency(EvalState & state, Value * * args, Value & v); void prim_unsafeDiscardOutputDependency(EvalState & state, Value * * args, Value & v);
namespace flake {
void prim_flakeRefToString(EvalState & state, Value * * args, Value & v);
void prim_getFlake(EvalState & state, Value * * args, Value & v);
void prim_parseFlakeRef(EvalState & state, Value * * args, Value & v);
}
} }
-11
View File
@@ -11,7 +11,6 @@ foreach header : [ 'imported-drv-to-derivation.nix', 'fetchurl.nix' ]
install_dir : includedir / 'lix/libexpr', install_dir : includedir / 'lix/libexpr',
) )
endforeach endforeach
subdir('flake')
libexpr_setting_definitions = files( libexpr_setting_definitions = files(
# keep-sorted start # keep-sorted start
@@ -79,7 +78,6 @@ builtin_definitions = files(
'builtins/filter.md', 'builtins/filter.md',
'builtins/filterSource.md', 'builtins/filterSource.md',
'builtins/findFile.md', 'builtins/findFile.md',
'builtins/flakeRefToString.md',
'builtins/floor.md', 'builtins/floor.md',
'builtins/foldlStrict.md', 'builtins/foldlStrict.md',
'builtins/fromJSON.md', 'builtins/fromJSON.md',
@@ -90,7 +88,6 @@ builtin_definitions = files(
'builtins/getAttr.md', 'builtins/getAttr.md',
'builtins/getContext.md', 'builtins/getContext.md',
'builtins/getEnv.md', 'builtins/getEnv.md',
'builtins/getFlake.md',
'builtins/groupBy.md', 'builtins/groupBy.md',
'builtins/hasAttr.md', 'builtins/hasAttr.md',
'builtins/hasContext.md', 'builtins/hasContext.md',
@@ -116,7 +113,6 @@ builtin_definitions = files(
'builtins/match.md', 'builtins/match.md',
'builtins/mul.md', 'builtins/mul.md',
'builtins/parseDrvName.md', 'builtins/parseDrvName.md',
'builtins/parseFlakeRef.md',
'builtins/partition.md', 'builtins/partition.md',
'builtins/path.md', 'builtins/path.md',
'builtins/pathExists.md', 'builtins/pathExists.md',
@@ -207,10 +203,6 @@ libexpr_sources = files(
'eval-error.cc', 'eval-error.cc',
'eval-settings.cc', 'eval-settings.cc',
'eval.cc', 'eval.cc',
'flake/config.cc',
'flake/flake.cc',
'flake/flakeref.cc',
'flake/lockfile.cc',
'function-trace.cc', 'function-trace.cc',
'gc-alloc.cc', 'gc-alloc.cc',
'get-drvs.cc', 'get-drvs.cc',
@@ -242,9 +234,6 @@ libexpr_headers = files(
'eval-inline.hh', 'eval-inline.hh',
'eval-settings.hh', 'eval-settings.hh',
'eval.hh', 'eval.hh',
'flake/flake.hh',
'flake/flakeref.hh',
'flake/lockfile.hh',
'function-trace.hh', 'function-trace.hh',
'gc-alloc.hh', 'gc-alloc.hh',
'gc-small-vector.hh', 'gc-small-vector.hh',
-1
View File
@@ -12,7 +12,6 @@ Pure evaluation mode ensures that the result of Nix expressions is fully determi
Access is nonetheless allowed to (absolute) paths in the Nix store that are returned by builtins like [`builtins.filterSource`](@docroot@/language/builtins.md#builtins-filterSource), [`builtins.fetchTarball`](@docroot@/language/builtins.md#builtins-fetchTarball) and similar. Access is nonetheless allowed to (absolute) paths in the Nix store that are returned by builtins like [`builtins.filterSource`](@docroot@/language/builtins.md#builtins-filterSource), [`builtins.fetchTarball`](@docroot@/language/builtins.md#builtins-fetchTarball) and similar.
- Impure fetches such as not specifying a commit ID for `builtins.fetchGit` or not specifying a hash for `builtins.fetchTarball` are rejected. - Impure fetches such as not specifying a commit ID for `builtins.fetchGit` or not specifying a hash for `builtins.fetchTarball` are rejected.
- In flakes, access to relative paths outside of the root of the flake's source tree (often, a git repository) is rejected.
- The evaluator ignores `NIX_PATH`, `-I` and the `nix-path` setting. Thus, [`builtins.nixPath`](@docroot@/language/builtin-constants.md#builtins-nixPath) is an empty list. - The evaluator ignores `NIX_PATH`, `-I` and the `nix-path` setting. Thus, [`builtins.nixPath`](@docroot@/language/builtin-constants.md#builtins-nixPath) is an empty list.
- The builtins [`builtins.currentSystem`](@docroot@/language/builtin-constants.md#builtins-currentSystem) and [`builtins.currentTime`](@docroot@/language/builtin-constants.md#builtins-currentTime) are absent from `builtins`. - The builtins [`builtins.currentSystem`](@docroot@/language/builtin-constants.md#builtins-currentSystem) and [`builtins.currentTime`](@docroot@/language/builtin-constants.md#builtins-currentTime) are absent from `builtins`.
- [`builtins.getEnv`](@docroot@/language/builtin-constants.md#builtins-currentSystem) always returns empty string for any variable. - [`builtins.getEnv`](@docroot@/language/builtin-constants.md#builtins-currentSystem) always returns empty string for any variable.
@@ -1,4 +1,4 @@
#include "lix/libexpr/flake/flake.hh" #include "lix/libflakes/expr/flake.hh"
#include "lix/libutil/logging.hh" #include "lix/libutil/logging.hh"
#include "lix/libutil/json.hh" #include "lix/libutil/json.hh"
#include "lix/libutil/users.hh" #include "lix/libutil/users.hh"
@@ -1,8 +1,9 @@
#include "lix/libexpr/flake/flake.hh" #include "lix/libflakes/expr/flake.hh"
#include "lix/libexpr/eval.hh" #include "lix/libexpr/eval.hh"
#include "lix/libexpr/eval-settings.hh" #include "lix/libexpr/eval-settings.hh"
#include "lix/libexpr/extra-primops.hh" #include "lix/libexpr/extra-primops.hh"
#include "lix/libexpr/flake/lockfile.hh" #include "lix/libexpr/primops.hh"
#include "lix/libflakes/expr/lockfile.hh"
#include "lix/libstore/store-api.hh" #include "lix/libstore/store-api.hh"
#include "lix/libfetchers/fetchers.hh" #include "lix/libfetchers/fetchers.hh"
#include "lix/libutil/async.hh" #include "lix/libutil/async.hh"
@@ -994,6 +995,12 @@ void prim_getFlake(EvalState & state, Value * * args, Value & v)
v); v);
} }
static RegisterPrimOp primop_getFlake(PrimOp{{
.name = "getFlake",
.arity = 1,
.fun = prim_getFlake,
}});
void prim_parseFlakeRef( void prim_parseFlakeRef(
EvalState & state, EvalState & state,
Value * * args, Value * * args,
@@ -1015,6 +1022,12 @@ void prim_parseFlakeRef(
v.mkAttrs(binds); v.mkAttrs(binds);
} }
static RegisterPrimOp primop_parseFlakeRef(PrimOp{{
.name = "parseFlakeRef",
.arity = 1,
.fun = prim_parseFlakeRef,
}});
void prim_flakeRefToString( void prim_flakeRefToString(
EvalState & state, EvalState & state,
Value * * args, Value * * args,
@@ -1053,6 +1066,12 @@ void prim_flakeRefToString(
v.mkString(flakeRef.to_string()); v.mkString(flakeRef.to_string());
} }
static RegisterPrimOp primop_flakeRefToString(PrimOp{{
.name = "flakeRefToString",
.arity = 1,
.fun = prim_flakeRefToString,
}});
} }
Fingerprint LockedFlake::getFingerprint() const Fingerprint LockedFlake::getFingerprint() const
@@ -3,8 +3,8 @@
#include "lix/libexpr/eval.hh" #include "lix/libexpr/eval.hh"
#include "lix/libutil/types.hh" #include "lix/libutil/types.hh"
#include "lix/libexpr/flake/flakeref.hh" #include "lix/libflakes/expr/flakeref.hh"
#include "lix/libexpr/flake/lockfile.hh" #include "lix/libflakes/expr/lockfile.hh"
#include "lix/libexpr/value.hh" #include "lix/libexpr/value.hh"
namespace nix { namespace nix {
@@ -1,4 +1,4 @@
#include "lix/libexpr/flake/flakeref.hh" #include "lix/libflakes/expr/flakeref.hh"
#include "lix/libstore/store-api.hh" #include "lix/libstore/store-api.hh"
#include "lix/libutil/async.hh" #include "lix/libutil/async.hh"
#include "lix/libutil/regex.hh" #include "lix/libutil/regex.hh"
@@ -1,4 +1,4 @@
#include "lix/libexpr/flake/lockfile.hh" #include "lix/libflakes/expr/lockfile.hh"
#include "lix/libstore/store-api.hh" #include "lix/libstore/store-api.hh"
#include "lix/libutil/error.hh" #include "lix/libutil/error.hh"
#include "lix/libutil/json.hh" #include "lix/libutil/json.hh"
@@ -1,7 +1,7 @@
#pragma once #pragma once
///@file ///@file
#include "lix/libexpr/flake/flakeref.hh" #include "lix/libflakes/expr/flakeref.hh"
#include "lix/libutil/json-fwd.hh" #include "lix/libutil/json-fwd.hh"
namespace nix { namespace nix {
@@ -1,8 +1,8 @@
libexpr_generated_headers += custom_target( libflakes_generated_headers += custom_target(
command : [ 'bash', '-c', 'echo \'R"__NIX_STR(\' | cat - @INPUT@ && echo \')__NIX_STR"\'' ], command : [ 'bash', '-c', 'echo \'R"__NIX_STR(\' | cat - @INPUT@ && echo \')__NIX_STR"\'' ],
input : 'call-flake.nix', input : 'call-flake.nix',
output : '@PLAINNAME@.gen.hh', output : '@PLAINNAME@.gen.hh',
capture : true, capture : true,
install : true, install : true,
install_dir : includedir / 'lix/libexpr/flake', install_dir : includedir / 'lix/libflakes/expr',
) )
+13
View File
@@ -0,0 +1,13 @@
#pragma once
///@file
namespace nix {
class EvalState;
struct Value;
namespace flake {
void prim_flakeRefToString(EvalState & state, Value * * args, Value & v);
void prim_getFlake(EvalState & state, Value * * args, Value & v);
void prim_parseFlakeRef(EvalState & state, Value * * args, Value & v);
}
}
+25
View File
@@ -0,0 +1,25 @@
#include "lix/libflakes/init.hh"
#include "lix/libflakes/expr/flakeref.hh"
#include "lix/libexpr/eval.hh"
#include "lix/libstore/store-api.hh"
// TODO: register the builtins
// register the setting for flakes (eval-cache)?
namespace nix::flake {
void initFlakes(Evaluator * eval)
{
// We inform the evaluator about Flakes during NIX_PATH processing.
eval->paths.registerAllowedNixPathPrefix("flake:");
eval->paths.registerSearchPathResolver([&](std::string const & value) {
return value.starts_with("flake:");
}, [&](std::string const & value, ref<Store> store) -> kj::Promise<Result<std::optional<std::string>>> {
auto flakeRef = parseFlakeRef(value.substr(6), {}, true, false);
debug("fetching flake search path element '%s'", value);
auto storePath =
TRY_AWAIT(TRY_AWAIT(flakeRef.resolve(store)).fetchTree(store)).first.storePath;
co_return {store->toRealPath(storePath)};
});
}
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
///@file
namespace nix {
class Evaluator;
namespace flake {
void initFlakes(Evaluator * eval);
}
}
+10
View File
@@ -0,0 +1,10 @@
prefix=@prefix@
libdir=@libdir@
includedir=@includedir@
Name: Lix libflakes
Description: Lix Package Manager (libflakes)
Version: @PACKAGE_VERSION@
# dependencies on boost is omitted since it is optional (only required by some headers)
Requires: lix-base lix-util lix-store lix-fetchers @BOEHM_IF_FOUND@
Libs: -L${libdir} -llixflakes
+157
View File
@@ -0,0 +1,157 @@
libflakes_generated_headers = []
subdir('expr')
# libflakes_setting_definitions = files(
# # keep-sorted start
# # keep-sorted end
# )
# libflakes_settings_header = custom_target(
# command : [
# python.full_path(),
# '@SOURCE_ROOT@/lix/code-generation/build_settings.py',
# '--kernel', host_machine.system(),
# '--header', '@OUTPUT@',
# '--experimental-features', '@SOURCE_ROOT@/lix/libutil/experimental-features',
# '@INPUT@',
# ],
# input : libflakes_setting_definitions,
# output : 'libflakes-settings.gen.inc',
# install : true,
# install_dir : includedir / 'lix/libflakes',
# )
builtin_definitions = files(
# keep-sorted start
'builtins/flakeRefToString.md',
'builtins/getFlake.md',
'builtins/parseFlakeRef.md',
# keep-sorted end
)
builtins_gen = custom_target(
command : [
python.full_path(),
'@SOURCE_ROOT@/lix/code-generation/build_builtins.py',
'--header', '@OUTPUT0@',
'--docs', '@OUTPUT1@',
'--experimental-features', '@SOURCE_ROOT@/lix/libutil/experimental-features',
'@INPUT@'
],
input : builtin_definitions,
output : [
'register-builtins.gen.inc',
'builtins.md',
],
)
register_builtins_header = builtins_gen[0]
builtins_md = builtins_gen[1]
libflakes_sources = files(
# keep-sorted start
'expr/config.cc',
'expr/flake.cc',
'expr/flakeref.cc',
'expr/lockfile.cc',
'init.cc',
# keep-sorted end
)
libflakes_headers = files(
# keep-sorted start
'expr/flake.hh',
'expr/flakeref.hh',
'expr/lockfile.hh',
'init.hh'
# keep-sorted end
)
dependencies = [
liblixutil,
liblixstore,
liblixfetchers,
liblixexpr,
boehm,
boost,
kj,
nlohmann_json,
toml11,
]
libflakes_temp = library(
is_static ? 'lixflakes_temp' : 'lixflakes',
libflakes_sources,
# libflakes_settings_header,
libflakes_generated_headers,
register_builtins_header,
dependencies : dependencies,
# for shared.hh
include_directories : [
'../libmain',
],
cpp_pch : cpp_pch,
install : not is_static,
# FIXME(Qyriad): is this right?
install_rpath : libdir,
)
# FIXME: remove when https://git.lix.systems/lix-project/lix/issues/359 is fixed.
# FIXME: replace by prelink when https://github.com/mesonbuild/meson/pull/14846 is widely available.
if is_static
libflakes_prelink = custom_target(
'lixflakes-prelink',
output : 'lixflakes-prelink.o',
input : libflakes_temp,
command : [
cxx.cmd_array(),
'-r',
'-o',
'@OUTPUT@',
is_darwin ? '-Wl,-force_load' : '-Wl,--whole-archive',
'@INPUT@',
],
)
libflakes = library(
'lixflakes',
[libflakes_prelink],
dependencies : dependencies,
install : true,
)
else
libflakes = libflakes_temp
endif
install_headers(
libflakes_headers,
subdir : 'lix/libflakes',
preserve_path : true,
)
liblixflakes = declare_dependency(
include_directories : include_directories('../..'),
# sources : libflakes_settings_header,
dependencies : [
liblixutil,
liblixexpr,
liblixfetchers,
boehm,
boost,
kj,
],
link_with : libflakes,
)
meson.override_dependency('lix-flakes', liblixflakes)
# FIXME: not using the pkg-config module because it creates way too many deps
# while meson migration is in progress, and we want to not include boost here
configure_file(
input : 'lix-flakes.pc.in',
output : 'lix-flakes.pc',
install_dir : libdir / 'pkgconfig',
configuration : {
'prefix' : prefix,
'libdir' : libdir,
'includedir' : includedir,
'PACKAGE_VERSION' : meson.project_version(),
'BOEHM_IF_FOUND' : boehm.found() ? 'bdw-gc' : '',
},
)
+5
View File
@@ -0,0 +1,5 @@
extern "C" void nix_plugin_entry()
{
// 1. register an allowed prefix for NIX_PATH parsing
// 2. register a search path resolver
}
+2
View File
@@ -22,6 +22,8 @@ subdir('libstore')
subdir('libfetchers') subdir('libfetchers')
# libexpr depends on all of the above # libexpr depends on all of the above
subdir('libexpr') subdir('libexpr')
# libflakes depends on all of the above
subdir('libflakes')
# libmain depends on libutil and libstore # libmain depends on libutil and libstore
subdir('libmain') subdir('libmain')
# libcmd depends on everything # libcmd depends on everything
+1 -1
View File
@@ -5,7 +5,7 @@
#include "lix/libexpr/eval.hh" #include "lix/libexpr/eval.hh"
#include "lix/libexpr/eval-inline.hh" // IWYU pragma: keep #include "lix/libexpr/eval-inline.hh" // IWYU pragma: keep
#include "lix/libexpr/eval-settings.hh" #include "lix/libexpr/eval-settings.hh"
#include "lix/libexpr/flake/flake.hh" #include "lix/libflakes/expr/flake.hh"
#include "lix/libexpr/get-drvs.hh" #include "lix/libexpr/get-drvs.hh"
#include "lix/libstore/store-api.hh" #include "lix/libstore/store-api.hh"
#include "lix/libstore/derivations.hh" #include "lix/libstore/derivations.hh"
+1
View File
@@ -172,6 +172,7 @@ nix = executable(
liblixstore, liblixstore,
liblixfetchers, liblixfetchers,
liblixexpr, liblixexpr,
liblixflakes,
liblixmain, liblixmain,
liblixcmd, liblixcmd,
boehm, boehm,
+1 -1
View File
@@ -7,7 +7,7 @@
#include "lix/libstore/derivations.hh" #include "lix/libstore/derivations.hh"
#include "lix/libutil/archive.hh" #include "lix/libutil/archive.hh"
#include "lix/libstore/builtins/buildenv.hh" #include "lix/libstore/builtins/buildenv.hh"
#include "lix/libexpr/flake/flakeref.hh" #include "lix/libflakes/expr/flakeref.hh"
#include "lix/libutil/regex.hh" #include "lix/libutil/regex.hh"
#include "user-env.hh" #include "user-env.hh"
#include "lix/libstore/profiles.hh" #include "lix/libstore/profiles.hh"
+1 -1
View File
@@ -2,7 +2,7 @@
#include "lix/libmain/common-args.hh" #include "lix/libmain/common-args.hh"
#include "lix/libmain/shared.hh" #include "lix/libmain/shared.hh"
#include "lix/libexpr/eval.hh" #include "lix/libexpr/eval.hh"
#include "lix/libexpr/flake/flake.hh" #include "lix/libflakes/expr/flake.hh"
#include "lix/libstore/store-api.hh" #include "lix/libstore/store-api.hh"
#include "lix/libfetchers/fetchers.hh" #include "lix/libfetchers/fetchers.hh"
#include "lix/libutil/url-parts.hh" #include "lix/libutil/url-parts.hh"
+9 -3
View File
@@ -112,6 +112,7 @@ endif
enable_nix_eval_jobs = get_option('nix-eval-jobs') enable_nix_eval_jobs = get_option('nix-eval-jobs')
enable_tests = get_option('enable-tests') enable_tests = get_option('enable-tests')
enable_contrib_plugins = get_option('enable-contrib-plugins')
tests_args = [] tests_args = []
@@ -229,12 +230,13 @@ is_x64 = host_machine.cpu_family() == 'x86_64'
# This corresponds to the $(1)_ALLOW_UNDEFINED option from the Make buildsystem. # 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. # Mostly this is load-bearing on the plugin tests defined in tests/functional/plugins/meson.build.
shared_module_link_args = [] shared_module_link_args = []
# This is a stricter additional set of link flags.
strict_shared_module_link_args = []
if is_darwin if is_darwin
shared_module_link_args += ['-undefined', 'suppress', '-flat_namespace'] shared_module_link_args += ['-undefined', 'suppress', '-flat_namespace']
strict_shared_module_link_args += ['-flat_namespace']
elif is_linux elif is_linux
# -Wl,-z,defs is the equivalent, but a comment in the Make buildsystem says that breaks strict_shared_module_link_args += ['-Wl,-z,defs']
# Clang sanitizers on Linux.
# FIXME(Qyriad): is that true?
endif endif
configdata = { } configdata = { }
@@ -694,6 +696,10 @@ if enable_tests
subdir('tests/functional2') subdir('tests/functional2')
endif endif
if enable_contrib_plugins
subdir('contrib/plugins')
endif
subdir('meson/clang-tidy') subdir('meson/clang-tidy')
subproject('nix-eval-jobs', required : enable_nix_eval_jobs) subproject('nix-eval-jobs', required : enable_nix_eval_jobs)
+4
View File
@@ -36,6 +36,10 @@ option('enable-tests', type : 'boolean', value : true,
description : 'whether to enable tests or not (requires rapidcheck and gtest)', 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, option('tests-color', type : 'boolean', value : true,
description : 'set to false to disable color output in gtest', description : 'set to false to disable color output in gtest',
) )
+1 -1
View File
@@ -36,7 +36,7 @@ pre-commit-run {
enable = true; enable = true;
package = pkgs.llvmPackages.libclang.python; package = pkgs.llvmPackages.libclang.python;
entry = "${pkgs.llvmPackages.libclang.python}/bin/git-clang-format --binary ${pkgs.llvmPackages.clang-tools}/bin/clang-format"; 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 = [ types = [
"c++" "c++"
"file" "file"
+5
View File
@@ -252,6 +252,7 @@ stdenv.mkDerivation (finalAttrs: {
./doc ./doc
./lix ./lix
./misc ./misc
./contrib/plugins
./COPYING ./COPYING
] ]
++ lib.optionals lintInsteadOfBuild [ ./.clang-tidy ] ++ lib.optionals lintInsteadOfBuild [ ./.clang-tidy ]
@@ -333,6 +334,10 @@ stdenv.mkDerivation (finalAttrs: {
yq yq
lsof lsof
zstd zstd
# For mTLS tests.
curl
openssl
python3
] ]
++ lib.optional hostPlatform.isLinux util-linuxMinimal ++ lib.optional hostPlatform.isLinux util-linuxMinimal
++ lib.optional (!officialRelease && buildUnreleasedNotes) build-release-notes ++ lib.optional (!officialRelease && buildUnreleasedNotes) build-release-notes
+1 -1
View File
@@ -10,7 +10,7 @@ if test -d "$TEST_ROOT"; then
killDaemon killDaemon
rm -rf "$TEST_ROOT" rm -rf "$TEST_ROOT"
fi fi
mkdir "$TEST_ROOT" mkdir -p "$TEST_ROOT"
mkdir "$NIX_STORE_DIR" mkdir "$NIX_STORE_DIR"
mkdir "$NIX_LOCALSTATE_DIR" mkdir "$NIX_LOCALSTATE_DIR"
+6
View File
@@ -173,6 +173,12 @@ if get_option('default_library') != 'static'
functional_tests_scripts += ['plugins.sh'] functional_tests_scripts += ['plugins.sh']
endif 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 # 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. # buildsystem. See the comments at the top of setup-functional-tests.py for why this is here.
meson.add_install_script( 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
@@ -1,16 +1,20 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include "lix/libexpr/flake/flakeref.hh" #include "lix/libflakes/expr/flakeref.hh"
#include "tests/libexpr.hh"
namespace nix { namespace nix {
class FlakeRefTest : public LibExprTest {};
/* ----------- tests for flake/flakeref.hh --------------------------------------------------*/ /* ----------- tests for flake/flakeref.hh --------------------------------------------------*/
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* to_string * to_string
* --------------------------------------------------------------------------*/ * --------------------------------------------------------------------------*/
TEST(to_string, doesntReencodeUrl) { TEST_F(FlakeRefTest, doesntReencodeUrl) {
auto s = "http://localhost:8181/test/+3d.tar.gz"; auto s = "http://localhost:8181/test/+3d.tar.gz";
auto flakeref = parseFlakeRef(s); auto flakeref = parseFlakeRef(s);
auto parsed = flakeref.to_string(); auto parsed = flakeref.to_string();
+36 -1
View File
@@ -207,7 +207,6 @@ libexpr_tests_sources = files(
'libexpr/attr-path.cc', 'libexpr/attr-path.cc',
'libexpr/derived-path.cc', 'libexpr/derived-path.cc',
'libexpr/error_traces.cc', 'libexpr/error_traces.cc',
'libexpr/flakeref.cc',
'libexpr/json.cc', 'libexpr/json.cc',
'libexpr/primops.cc', 'libexpr/primops.cc',
'libexpr/search-path.cc', 'libexpr/search-path.cc',
@@ -247,6 +246,42 @@ test(
verbose : false, verbose : false,
) )
libflakes_tests_sources = files(
'libflakes/flakeref.cc',
)
libflakes_tester = executable(
'liblixflakes-tests',
libflakes_tests_sources,
dependencies : [
libasanoptions,
liblixexpr_test_support,
liblixstore_test_support,
liblixstore,
liblixutil,
liblixexpr,
liblixfetchers,
liblixflakes,
rapidcheck,
gtest,
nlohmann_json,
kj
],
cpp_pch : cpp_pch
)
test(
'libflakes-unit-tests',
libflakes_tester,
args : tests_args,
env : default_test_env + {
'_NIX_TEST_UNIT_DATA': meson.project_source_root() / 'tests/unit/libflakes/data',
},
suite : 'check',
protocol : 'gtest',
verbose : false,
)
libcmd_tester = executable( libcmd_tester = executable(
'liblixcmd-tests', 'liblixcmd-tests',
files('libcmd/args.cc'), files('libcmd/args.cc'),