Skip to content

Commit 647ac1c

Browse files
committed
feat(libstore): curl-based s3
1 parent 8e5ca78 commit 647ac1c

File tree

5 files changed

+320
-6
lines changed

5 files changed

+320
-6
lines changed

src/libstore/aws-auth.cc

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#include "nix/store/aws-auth.hh"
2+
#include "nix/store/config.hh"
3+
4+
#if NIX_WITH_AWS_CRT_SUPPORT
5+
6+
# include "nix/util/logging.hh"
7+
# include "nix/util/finally.hh"
8+
9+
# include <aws/crt/Api.h>
10+
# include <aws/crt/auth/Credentials.h>
11+
# include <aws/crt/io/Bootstrap.h>
12+
13+
# include <condition_variable>
14+
# include <mutex>
15+
16+
namespace nix {
17+
18+
static std::once_flag crtInitFlag;
19+
static std::unique_ptr<Aws::Crt::ApiHandle> crtApiHandle;
20+
21+
static void initAwsCrt()
22+
{
23+
std::call_once(crtInitFlag, []() {
24+
crtApiHandle = std::make_unique<Aws::Crt::ApiHandle>();
25+
crtApiHandle->InitializeLogging(Aws::Crt::LogLevel::Warn, (FILE *) nullptr);
26+
});
27+
}
28+
29+
std::unique_ptr<AwsCredentialProvider> AwsCredentialProvider::createDefault()
30+
{
31+
initAwsCrt();
32+
33+
Aws::Crt::Auth::CredentialsProviderChainDefaultConfig config;
34+
config.Bootstrap = Aws::Crt::ApiHandle::GetOrCreateStaticDefaultClientBootstrap();
35+
36+
auto provider = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderChainDefault(config);
37+
if (!provider) {
38+
debug("Failed to create default AWS credentials provider");
39+
return nullptr;
40+
}
41+
42+
return std::make_unique<AwsCredentialProvider>(provider);
43+
}
44+
45+
std::unique_ptr<AwsCredentialProvider> AwsCredentialProvider::createProfile(const std::string & profile)
46+
{
47+
initAwsCrt();
48+
49+
if (profile.empty()) {
50+
return createDefault();
51+
}
52+
53+
Aws::Crt::Auth::CredentialsProviderProfileConfig config;
54+
config.Bootstrap = Aws::Crt::ApiHandle::GetOrCreateStaticDefaultClientBootstrap();
55+
config.ProfileNameOverride = Aws::Crt::ByteCursorFromCString(profile.c_str());
56+
57+
auto provider = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderProfile(config);
58+
if (!provider) {
59+
debug("Failed to create AWS credentials provider for profile '%s'", profile);
60+
return nullptr;
61+
}
62+
63+
return std::make_unique<AwsCredentialProvider>(provider);
64+
}
65+
66+
AwsCredentialProvider::AwsCredentialProvider(std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider> provider)
67+
: provider(provider)
68+
{
69+
}
70+
71+
AwsCredentialProvider::~AwsCredentialProvider() = default;
72+
73+
std::optional<AwsCredentials> AwsCredentialProvider::getCredentials()
74+
{
75+
if (!provider || !provider->IsValid()) {
76+
debug("AWS credential provider is invalid");
77+
return std::nullopt;
78+
}
79+
80+
std::mutex mutex;
81+
std::condition_variable cv;
82+
std::optional<AwsCredentials> result;
83+
bool resolved = false;
84+
85+
provider->GetCredentials([&](std::shared_ptr<Aws::Crt::Auth::Credentials> credentials, int errorCode) {
86+
std::lock_guard<std::mutex> lock(mutex);
87+
88+
if (errorCode != 0 || !credentials) {
89+
debug("Failed to resolve AWS credentials: error code %d", errorCode);
90+
} else {
91+
auto accessKeyId = credentials->GetAccessKeyId();
92+
auto secretAccessKey = credentials->GetSecretAccessKey();
93+
auto sessionToken = credentials->GetSessionToken();
94+
95+
std::optional<std::string> sessionTokenStr;
96+
if (sessionToken.len > 0) {
97+
sessionTokenStr = std::string(reinterpret_cast<const char *>(sessionToken.ptr), sessionToken.len);
98+
}
99+
100+
result = AwsCredentials(
101+
std::string(reinterpret_cast<const char *>(accessKeyId.ptr), accessKeyId.len),
102+
std::string(reinterpret_cast<const char *>(secretAccessKey.ptr), secretAccessKey.len),
103+
sessionTokenStr);
104+
}
105+
106+
resolved = true;
107+
cv.notify_one();
108+
});
109+
110+
std::unique_lock<std::mutex> lock(mutex);
111+
cv.wait(lock, [&] { return resolved; });
112+
113+
return result;
114+
}
115+
116+
} // namespace nix
117+
118+
#endif // NIX_WITH_AWS_CRT_SUPPORT

src/libstore/filetransfer.cc

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
#include "nix/util/finally.hh"
88
#include "nix/util/callback.hh"
99
#include "nix/util/signals.hh"
10+
#include "nix/store/store-reference.hh"
1011

1112
#include "store-config-private.hh"
1213
#if NIX_WITH_S3_SUPPORT
1314
# include <aws/core/client/ClientConfiguration.h>
1415
#endif
16+
#if NIX_WITH_AWS_CRT_SUPPORT
17+
# include "nix/store/aws-auth.hh"
18+
#endif
1519

1620
#ifdef __linux__
1721
# include "nix/util/linux-namespaces.hh"
@@ -77,6 +81,13 @@ struct curlFileTransfer : public FileTransfer
7781

7882
std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now();
7983

84+
#if NIX_WITH_AWS_CRT_SUPPORT
85+
// AWS SigV4 authentication data
86+
bool isS3Request = false;
87+
std::string awsCredentials; // "access_key:secret_key" for CURLOPT_USERPWD
88+
std::string awsSigV4Provider; // Provider string for CURLOPT_AWS_SIGV4
89+
#endif
90+
8091
inline static const std::set<long> successfulStatuses{200, 201, 204, 206, 304, 0 /* other protocol */};
8192

8293
/* Get the HTTP status code, or 0 for other protocols. */
@@ -131,6 +142,52 @@ struct curlFileTransfer : public FileTransfer
131142
for (auto it = request.headers.begin(); it != request.headers.end(); ++it) {
132143
requestHeaders = curl_slist_append(requestHeaders, fmt("%s: %s", it->first, it->second).c_str());
133144
}
145+
146+
#if NIX_WITH_AWS_CRT_SUPPORT && NIX_WITH_S3_SUPPORT
147+
// Handle S3 URLs with curl-based AWS SigV4 authentication
148+
if (hasPrefix(request.uri, "s3://")) {
149+
try {
150+
auto [httpsUri, params] = fileTransfer.convertS3ToHttpsUri(request.uri);
151+
152+
// Update the request URI to use HTTPS
153+
const_cast<FileTransferRequest &>(request).uri = httpsUri;
154+
result.urls.clear();
155+
result.urls.push_back(httpsUri);
156+
157+
isS3Request = true;
158+
159+
// Get credentials
160+
std::string profile = getOr(params, "profile", "");
161+
auto credProvider = profile.empty() ? AwsCredentialProvider::createDefault()
162+
: AwsCredentialProvider::createProfile(profile);
163+
164+
if (credProvider) {
165+
auto creds = credProvider->getCredentials();
166+
if (creds) {
167+
awsCredentials = creds->accessKeyId + ":" + creds->secretAccessKey;
168+
169+
std::string region = getOr(params, "region", "us-east-1");
170+
std::string service = "s3";
171+
awsSigV4Provider = "aws:amz:" + region + ":" + service;
172+
173+
// Add session token header if present
174+
if (creds->sessionToken) {
175+
requestHeaders = curl_slist_append(
176+
requestHeaders, ("x-amz-security-token: " + *creds->sessionToken).c_str());
177+
}
178+
179+
debug("Using AWS SigV4 authentication for S3 request to %s", httpsUri);
180+
} else {
181+
warn("Failed to obtain AWS credentials for S3 request %s", request.uri);
182+
}
183+
} else {
184+
warn("Failed to create AWS credential provider for S3 request %s", request.uri);
185+
}
186+
} catch (std::exception & e) {
187+
warn("Failed to set up AWS SigV4 authentication for S3 request %s: %s", request.uri, e.what());
188+
}
189+
}
190+
#endif
134191
}
135192

136193
~TransferItem()
@@ -426,6 +483,15 @@ struct curlFileTransfer : public FileTransfer
426483
curl_easy_setopt(req, CURLOPT_ERRORBUFFER, errbuf);
427484
errbuf[0] = 0;
428485

486+
#if NIX_WITH_AWS_CRT_SUPPORT && LIBCURL_VERSION_NUM >= 0x074b00 // curl 7.75.0
487+
// Set up AWS SigV4 authentication if this is an S3 request
488+
if (isS3Request && !awsCredentials.empty() && !awsSigV4Provider.empty()) {
489+
curl_easy_setopt(req, CURLOPT_USERPWD, awsCredentials.c_str());
490+
curl_easy_setopt(req, CURLOPT_AWS_SIGV4, awsSigV4Provider.c_str());
491+
debug("Configured curl with AWS SigV4 authentication: provider=%s", awsSigV4Provider);
492+
}
493+
#endif
494+
429495
result.data.clear();
430496
result.bodySize = 0;
431497
}
@@ -812,15 +878,42 @@ struct curlFileTransfer : public FileTransfer
812878

813879
return {bucketName, key, params};
814880
}
881+
882+
/**
883+
* Convert S3 URI to HTTPS URI for use with curl's AWS SigV4 authentication
884+
*/
885+
std::pair<std::string, Store::Config::Params> convertS3ToHttpsUri(const std::string & s3Uri)
886+
{
887+
auto [bucketName, key, params] = parseS3Uri(s3Uri);
888+
889+
std::string region = getOr(params, "region", "us-east-1");
890+
std::string endpoint = getOr(params, "endpoint", "");
891+
std::string scheme = getOr(params, "scheme", "https");
892+
893+
std::string httpsUri;
894+
if (!endpoint.empty()) {
895+
// Custom endpoint (e.g., MinIO, custom S3-compatible service)
896+
httpsUri = scheme + "://" + endpoint + "/" + bucketName + "/" + key;
897+
} else {
898+
// Standard AWS S3 endpoint
899+
httpsUri = scheme + "://s3." + region + ".amazonaws.com/" + bucketName + "/" + key;
900+
}
901+
902+
return {httpsUri, params};
903+
}
815904
#endif
816905

817906
void enqueueFileTransfer(const FileTransferRequest & request, Callback<FileTransferResult> callback) override
818907
{
819-
/* Ugly hack to support s3:// URIs. */
908+
/* Handle s3:// URIs with curl-based AWS SigV4 authentication or fall back to legacy S3Helper */
820909
if (hasPrefix(request.uri, "s3://")) {
910+
#if NIX_WITH_AWS_CRT_SUPPORT && LIBCURL_VERSION_NUM >= 0x074b00
911+
// Use new curl-based approach with AWS SigV4 authentication
912+
enqueueItem(std::make_shared<TransferItem>(*this, request, std::move(callback)));
913+
#elif NIX_WITH_S3_SUPPORT
914+
// Fall back to legacy S3Helper approach
821915
// FIXME: do this on a worker thread
822916
try {
823-
#if NIX_WITH_S3_SUPPORT
824917
auto [bucketName, key, params] = parseS3Uri(request.uri);
825918

826919
std::string profile = getOr(params, "profile", "");
@@ -838,12 +931,12 @@ struct curlFileTransfer : public FileTransfer
838931
res.data = std::move(*s3Res.data);
839932
res.urls.push_back(request.uri);
840933
callback(std::move(res));
841-
#else
842-
throw nix::Error("cannot download '%s' because Nix is not built with S3 support", request.uri);
843-
#endif
844934
} catch (...) {
845935
callback.rethrow();
846936
}
937+
#else
938+
throw nix::Error("cannot download '%s' because Nix is not built with S3 support", request.uri);
939+
#endif
847940
return;
848941
}
849942

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#pragma once
2+
///@file
3+
4+
#include "nix/util/types.hh"
5+
#include "nix/util/ref.hh"
6+
#include "nix/store/config.hh"
7+
8+
#include <memory>
9+
#include <optional>
10+
#include <string>
11+
12+
#if NIX_WITH_AWS_CRT_SUPPORT
13+
14+
namespace Aws {
15+
namespace Crt {
16+
namespace Auth {
17+
class ICredentialsProvider;
18+
class Credentials;
19+
} // namespace Auth
20+
} // namespace Crt
21+
} // namespace Aws
22+
23+
namespace nix {
24+
25+
/**
26+
* AWS credentials obtained from credential providers
27+
*/
28+
struct AwsCredentials
29+
{
30+
std::string accessKeyId;
31+
std::string secretAccessKey;
32+
std::optional<std::string> sessionToken;
33+
34+
AwsCredentials(
35+
const std::string & accessKeyId,
36+
const std::string & secretAccessKey,
37+
const std::optional<std::string> & sessionToken = std::nullopt)
38+
: accessKeyId(accessKeyId)
39+
, secretAccessKey(secretAccessKey)
40+
, sessionToken(sessionToken)
41+
{
42+
}
43+
};
44+
45+
/**
46+
* AWS credential provider wrapper using aws-crt-cpp
47+
* Provides lightweight credential resolution without full AWS SDK dependency
48+
*/
49+
class AwsCredentialProvider
50+
{
51+
private:
52+
std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider> provider;
53+
54+
public:
55+
/**
56+
* Create credential provider using the default AWS credential chain
57+
* This includes: Environment -> Profile -> IMDS/ECS
58+
*/
59+
static std::unique_ptr<AwsCredentialProvider> createDefault();
60+
61+
/**
62+
* Create credential provider for a specific profile
63+
*/
64+
static std::unique_ptr<AwsCredentialProvider> createProfile(const std::string & profile);
65+
66+
/**
67+
* Get credentials synchronously
68+
* Returns nullopt if credentials cannot be resolved
69+
*/
70+
std::optional<AwsCredentials> getCredentials();
71+
72+
AwsCredentialProvider(std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider> provider);
73+
~AwsCredentialProvider();
74+
};
75+
76+
} // namespace nix
77+
78+
#endif // NIX_WITH_AWS_CRT_SUPPORT

src/libstore/meson.build

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,23 @@ if aws_s3.found()
159159
endif
160160
deps_other += aws_s3
161161

162+
# AWS CRT C++ for lightweight credential management
163+
# Similar to aws-cpp-sdk, but lighter weight dependency for credential resolution only
164+
aws_crt_cpp = dependency('aws-crt-cpp', required : false)
165+
if not aws_crt_cpp.found()
166+
# Fallback: try pkg-config with different name
167+
aws_crt_cpp = dependency('libaws-crt-cpp', required : false)
168+
endif
169+
if not aws_crt_cpp.found()
170+
# Fallback: try to find library manually
171+
aws_crt_cpp_lib = cxx.find_library('aws-crt-cpp', required : false)
172+
if aws_crt_cpp_lib.found()
173+
aws_crt_cpp = aws_crt_cpp_lib
174+
endif
175+
endif
176+
configdata_pub.set('NIX_WITH_AWS_CRT_SUPPORT', aws_crt_cpp.found().to_int())
177+
deps_other += aws_crt_cpp
178+
162179
subdir('nix-meson-build-support/generate-header')
163180

164181
generated_headers = []
@@ -262,6 +279,7 @@ config_priv_h = configure_file(
262279
subdir('nix-meson-build-support/common')
263280

264281
sources = files(
282+
'aws-auth.cc',
265283
'binary-cache-store.cc',
266284
'build-result.cc',
267285
'build/derivation-building-goal.cc',

0 commit comments

Comments
 (0)