Skip to content

Commit 00c2a57

Browse files
committed
feat(libstore/filetransfer): add S3 signing support
1 parent 3c1e2e5 commit 00c2a57

File tree

4 files changed

+100
-24
lines changed

4 files changed

+100
-24
lines changed

src/libstore/aws-creds.cc

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ namespace nix {
2424

2525
namespace {
2626

27+
// Global credential provider cache using boost's concurrent map
28+
// Key: profile name (empty string for default profile)
29+
using CredentialProviderCache =
30+
boost::concurrent_flat_map<std::string, std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider>>;
31+
32+
static CredentialProviderCache credentialProviderCache;
33+
34+
/**
35+
* Clear all cached credential providers.
36+
* Called automatically by CrtWrapper destructor during static destruction.
37+
*/
38+
static void clearAwsCredentialsCache()
39+
{
40+
credentialProviderCache.clear();
41+
}
42+
2743
static void initAwsCrt()
2844
{
2945
struct CrtWrapper
@@ -95,13 +111,6 @@ static AwsCredentials getCredentialsFromProvider(std::shared_ptr<Aws::Crt::Auth:
95111
return fut.get(); // This will throw if set_exception was called
96112
}
97113

98-
// Global credential provider cache using boost's concurrent map
99-
// Key: profile name (empty string for default profile)
100-
using CredentialProviderCache =
101-
boost::concurrent_flat_map<std::string, std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider>>;
102-
103-
static CredentialProviderCache credentialProviderCache;
104-
105114
} // anonymous namespace
106115

107116
AwsCredentials getAwsCredentials(const std::string & profile)
@@ -160,11 +169,6 @@ void invalidateAwsCredentials(const std::string & profile)
160169
credentialProviderCache.erase(profile);
161170
}
162171

163-
void clearAwsCredentialsCache()
164-
{
165-
credentialProviderCache.clear();
166-
}
167-
168172
AwsCredentials preResolveAwsCredentials(const ParsedS3URL & s3Url)
169173
{
170174
std::string profile = s3Url.profile.value_or("");

src/libstore/filetransfer.cc

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
#include "nix/util/signals.hh"
1010

1111
#include "store-config-private.hh"
12+
#include <optional>
1213
#if NIX_WITH_S3_SUPPORT
1314
# include <aws/core/client/ClientConfiguration.h>
1415
#endif
16+
#if NIX_WITH_CURL_S3
17+
# include "nix/store/aws-creds.hh"
18+
# include "nix/store/s3-url.hh"
19+
#endif
1520

1621
#ifdef __linux__
1722
# include "nix/util/linux-namespaces.hh"
@@ -434,6 +439,16 @@ struct curlFileTransfer : public FileTransfer
434439
}
435440
}
436441

442+
#if NIX_WITH_CURL_S3
443+
// Set up AWS SigV4 signing if this is an S3 request
444+
// Note: AWS SigV4 support guaranteed available (curl >= 7.75.0 checked at build time)
445+
// The username/password (access key ID and secret key) are set via the general
446+
// usernameAuth mechanism above.
447+
if (request.awsSigV4Provider) {
448+
curl_easy_setopt(req, CURLOPT_AWS_SIGV4, request.awsSigV4Provider->c_str());
449+
}
450+
#endif
451+
437452
result.data.clear();
438453
result.bodySize = 0;
439454
}
@@ -808,7 +823,11 @@ struct curlFileTransfer : public FileTransfer
808823

809824
void enqueueItem(std::shared_ptr<TransferItem> item)
810825
{
811-
if (item->request.data && item->request.uri.scheme() != "http" && item->request.uri.scheme() != "https")
826+
if (item->request.data && item->request.uri.scheme() != "http" && item->request.uri.scheme() != "https"
827+
#if NIX_WITH_CURL_S3
828+
&& item->request.uri.scheme() != "s3"
829+
#endif
830+
)
812831
throw nix::Error("uploading to '%s' is not supported", item->request.uri.to_string());
813832

814833
{
@@ -826,9 +845,15 @@ struct curlFileTransfer : public FileTransfer
826845
{
827846
/* Ugly hack to support s3:// URIs. */
828847
if (request.uri.scheme() == "s3") {
848+
#if NIX_WITH_CURL_S3
849+
// New curl-based S3 implementation
850+
auto modifiedRequest = request;
851+
modifiedRequest.setupForS3();
852+
enqueueItem(std::make_shared<TransferItem>(*this, std::move(modifiedRequest), std::move(callback)));
853+
#elif NIX_WITH_S3_SUPPORT
854+
// Old AWS SDK-based implementation
829855
// FIXME: do this on a worker thread
830856
try {
831-
#if NIX_WITH_S3_SUPPORT
832857
auto parsed = ParsedS3URL::parse(request.uri.parsed());
833858

834859
std::string profile = parsed.profile.value_or("");
@@ -846,13 +871,12 @@ struct curlFileTransfer : public FileTransfer
846871
res.data = std::move(*s3Res.data);
847872
res.urls.push_back(request.uri.to_string());
848873
callback(std::move(res));
849-
#else
850-
throw nix::Error(
851-
"cannot download '%s' because Nix is not built with S3 support", request.uri.to_string());
852-
#endif
853874
} catch (...) {
854875
callback.rethrow();
855876
}
877+
#else
878+
throw nix::Error("cannot download '%s' because Nix is not built with S3 support", request.uri.to_string());
879+
#endif
856880
return;
857881
}
858882

@@ -880,6 +904,41 @@ ref<FileTransfer> makeFileTransfer()
880904
return makeCurlFileTransfer();
881905
}
882906

907+
#if NIX_WITH_CURL_S3
908+
void FileTransferRequest::setupForS3()
909+
{
910+
auto parsedS3 = ParsedS3URL::parse(uri.parsed());
911+
// Update the request URI to use HTTPS
912+
uri = parsedS3.toHttpsUrl();
913+
// This gets used later in a curl setopt
914+
awsSigV4Provider = "aws:amz:" + parsedS3.region.value_or("us-east-1") + ":s3";
915+
// check if the request already has pre-resolved credentials
916+
std::optional<std::string> sessionToken;
917+
if (usernameAuth) {
918+
debug("Using pre-resolved AWS credentials from parent process");
919+
sessionToken = preResolvedAwsSessionToken;
920+
} else {
921+
std::string profile = parsedS3.profile.value_or("");
922+
try {
923+
auto creds = getAwsCredentials(profile);
924+
usernameAuth = UsernameAuth{
925+
.username = creds.accessKeyId,
926+
.password = creds.secretAccessKey,
927+
};
928+
sessionToken = creds.sessionToken;
929+
} catch (const AwsAuthError & e) {
930+
warn("AWS authentication failed for S3 request %s: %s", uri, e.what());
931+
// Invalidate the cached credentials so next request will retry
932+
invalidateAwsCredentials(profile);
933+
// Continue without authentication - might be a public bucket
934+
return;
935+
}
936+
}
937+
if (sessionToken)
938+
headers.emplace_back("x-amz-security-token", *sessionToken);
939+
}
940+
#endif
941+
883942
std::future<FileTransferResult> FileTransfer::enqueueFileTransfer(const FileTransferRequest & request)
884943
{
885944
auto promise = std::make_shared<std::promise<FileTransferResult>>();

src/libstore/include/nix/store/aws-creds.hh

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,6 @@ AwsCredentials getAwsCredentials(const std::string & profile = "");
5757
*/
5858
void invalidateAwsCredentials(const std::string & profile);
5959

60-
/**
61-
* Clear all cached credential providers.
62-
* Typically called during application cleanup.
63-
*/
64-
void clearAwsCredentialsCache();
65-
6660
/**
6761
* Pre-resolve AWS credentials for S3 URLs.
6862
* Used to cache credentials in parent process before forking.

src/libstore/include/nix/store/filetransfer.hh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
#include "nix/util/serialise.hh"
1212
#include "nix/util/url.hh"
1313

14+
#include "nix/store/config.hh"
15+
#if NIX_WITH_CURL_S3
16+
# include "nix/store/aws-creds.hh"
17+
#endif
18+
1419
namespace nix {
1520

1621
struct FileTransferSettings : Config
@@ -108,6 +113,13 @@ struct FileTransferRequest
108113
* When provided, these credentials will be used with curl's CURLOPT_USERNAME/PASSWORD option.
109114
*/
110115
std::optional<UsernameAuth> usernameAuth;
116+
#if NIX_WITH_CURL_S3
117+
/**
118+
* Pre-resolved AWS session token for S3 requests.
119+
* When provided along with usernameAuth, this will be used instead of fetching fresh credentials.
120+
*/
121+
std::optional<std::string> preResolvedAwsSessionToken;
122+
#endif
111123

112124
FileTransferRequest(ValidURL uri)
113125
: uri(std::move(uri))
@@ -119,6 +131,13 @@ struct FileTransferRequest
119131
{
120132
return data ? "upload" : "download";
121133
}
134+
135+
#if NIX_WITH_CURL_S3
136+
private:
137+
friend struct curlFileTransfer;
138+
void setupForS3();
139+
std::optional<std::string> awsSigV4Provider;
140+
#endif
122141
};
123142

124143
struct FileTransferResult

0 commit comments

Comments
 (0)