// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #include // Platform-specific crypto backends #if ZEN_PLATFORM_WINDOWS # define ZEN_S3_USE_BCRYPT 1 #else # define ZEN_S3_USE_BCRYPT 0 #endif #ifndef ZEN_S3_USE_OPENSSL # if ZEN_S3_USE_BCRYPT # define ZEN_S3_USE_OPENSSL 0 # else # define ZEN_S3_USE_OPENSSL 1 # endif #endif ZEN_THIRD_PARTY_INCLUDES_START #include #if ZEN_S3_USE_OPENSSL # include #elif ZEN_S3_USE_BCRYPT # include # include #endif ZEN_THIRD_PARTY_INCLUDES_END namespace zen { ////////////////////////////////////////////////////////////////////////// // SHA-256 #if ZEN_S3_USE_OPENSSL Sha256Digest ComputeSha256(const void* Data, size_t Size) { Sha256Digest Result; unsigned int Len = 0; EVP_Digest(Data, Size, Result.data(), &Len, EVP_sha256(), nullptr); ZEN_ASSERT(Len == 32); return Result; } Sha256Stream::Sha256Stream() { EVP_MD_CTX* Ctx = EVP_MD_CTX_new(); ZEN_ASSERT(Ctx != nullptr); int Rc = EVP_DigestInit_ex(Ctx, EVP_sha256(), nullptr); ZEN_ASSERT(Rc == 1); m_Ctx = Ctx; } Sha256Stream::~Sha256Stream() { if (m_Ctx) { EVP_MD_CTX_free(static_cast(m_Ctx)); m_Ctx = nullptr; } } Sha256Stream::Sha256Stream(Sha256Stream&& Other) noexcept : m_Ctx(Other.m_Ctx) { Other.m_Ctx = nullptr; } Sha256Stream& Sha256Stream::operator=(Sha256Stream&& Other) noexcept { if (this != &Other) { if (m_Ctx) { EVP_MD_CTX_free(static_cast(m_Ctx)); } m_Ctx = Other.m_Ctx; Other.m_Ctx = nullptr; } return *this; } void Sha256Stream::Update(const void* Data, size_t Size) { int Rc = EVP_DigestUpdate(static_cast(m_Ctx), Data, Size); ZEN_ASSERT(Rc == 1); } Sha256Digest Sha256Stream::Finalize() { Sha256Digest Result; unsigned int Len = 0; int Rc = EVP_DigestFinal_ex(static_cast(m_Ctx), Result.data(), &Len); ZEN_ASSERT(Rc == 1); ZEN_ASSERT(Len == 32); return Result; } Sha256Digest ComputeHmacSha256(const void* Key, size_t KeySize, const void* Data, size_t DataSize) { Sha256Digest Result; EVP_MAC* Mac = EVP_MAC_fetch(nullptr, "HMAC", nullptr); ZEN_ASSERT(Mac != nullptr); EVP_MAC_CTX* Ctx = EVP_MAC_CTX_new(Mac); ZEN_ASSERT(Ctx != nullptr); OSSL_PARAM Params[] = { OSSL_PARAM_construct_utf8_string("digest", const_cast("SHA256"), 0), OSSL_PARAM_construct_end(), }; int Rc = EVP_MAC_init(Ctx, reinterpret_cast(Key), KeySize, Params); ZEN_ASSERT(Rc == 1); Rc = EVP_MAC_update(Ctx, reinterpret_cast(Data), DataSize); ZEN_ASSERT(Rc == 1); size_t OutLen = 0; Rc = EVP_MAC_final(Ctx, Result.data(), &OutLen, Result.size()); ZEN_ASSERT(Rc == 1); ZEN_ASSERT(OutLen == 32); EVP_MAC_CTX_free(Ctx); EVP_MAC_free(Mac); return Result; } #elif ZEN_S3_USE_BCRYPT namespace { # define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) Sha256Digest BcryptHash(BCRYPT_ALG_HANDLE Algorithm, const void* Data, size_t DataSize) { Sha256Digest Result; BCRYPT_HASH_HANDLE HashHandle = nullptr; NTSTATUS Status; Status = BCryptCreateHash(Algorithm, &HashHandle, nullptr, 0, nullptr, 0, 0); ZEN_ASSERT(NT_SUCCESS(Status)); Status = BCryptHashData(HashHandle, (PUCHAR)Data, (ULONG)DataSize, 0); ZEN_ASSERT(NT_SUCCESS(Status)); Status = BCryptFinishHash(HashHandle, Result.data(), (ULONG)Result.size(), 0); ZEN_ASSERT(NT_SUCCESS(Status)); BCryptDestroyHash(HashHandle); return Result; } Sha256Digest BcryptHmac(BCRYPT_ALG_HANDLE Algorithm, const void* Key, size_t KeySize, const void* Data, size_t DataSize) { Sha256Digest Result; BCRYPT_HASH_HANDLE HashHandle = nullptr; NTSTATUS Status; Status = BCryptCreateHash(Algorithm, &HashHandle, nullptr, 0, (PUCHAR)Key, (ULONG)KeySize, 0); ZEN_ASSERT(NT_SUCCESS(Status)); Status = BCryptHashData(HashHandle, (PUCHAR)Data, (ULONG)DataSize, 0); ZEN_ASSERT(NT_SUCCESS(Status)); Status = BCryptFinishHash(HashHandle, Result.data(), (ULONG)Result.size(), 0); ZEN_ASSERT(NT_SUCCESS(Status)); BCryptDestroyHash(HashHandle); return Result; } struct BcryptAlgorithmHandles { BCRYPT_ALG_HANDLE Sha256 = nullptr; BCRYPT_ALG_HANDLE HmacSha256 = nullptr; BcryptAlgorithmHandles() { NTSTATUS Status; Status = BCryptOpenAlgorithmProvider(&Sha256, BCRYPT_SHA256_ALGORITHM, nullptr, 0); ZEN_ASSERT(NT_SUCCESS(Status)); Status = BCryptOpenAlgorithmProvider(&HmacSha256, BCRYPT_SHA256_ALGORITHM, nullptr, BCRYPT_ALG_HANDLE_HMAC_FLAG); ZEN_ASSERT(NT_SUCCESS(Status)); } ~BcryptAlgorithmHandles() { if (Sha256) { BCryptCloseAlgorithmProvider(Sha256, 0); } if (HmacSha256) { BCryptCloseAlgorithmProvider(HmacSha256, 0); } } }; BcryptAlgorithmHandles& GetBcryptHandles() { static BcryptAlgorithmHandles s_Handles; return s_Handles; } } // namespace Sha256Digest ComputeSha256(const void* Data, size_t Size) { return BcryptHash(GetBcryptHandles().Sha256, Data, Size); } Sha256Stream::Sha256Stream() { BCRYPT_HASH_HANDLE Handle = nullptr; NTSTATUS Status = BCryptCreateHash(GetBcryptHandles().Sha256, &Handle, nullptr, 0, nullptr, 0, 0); ZEN_ASSERT(NT_SUCCESS(Status)); m_Ctx = Handle; } Sha256Stream::~Sha256Stream() { if (m_Ctx) { BCryptDestroyHash(static_cast(m_Ctx)); m_Ctx = nullptr; } } Sha256Stream::Sha256Stream(Sha256Stream&& Other) noexcept : m_Ctx(Other.m_Ctx) { Other.m_Ctx = nullptr; } Sha256Stream& Sha256Stream::operator=(Sha256Stream&& Other) noexcept { if (this != &Other) { if (m_Ctx) { BCryptDestroyHash(static_cast(m_Ctx)); } m_Ctx = Other.m_Ctx; Other.m_Ctx = nullptr; } return *this; } void Sha256Stream::Update(const void* Data, size_t Size) { NTSTATUS Status = BCryptHashData(static_cast(m_Ctx), reinterpret_cast(const_cast(Data)), static_cast(Size), 0); ZEN_ASSERT(NT_SUCCESS(Status)); } Sha256Digest Sha256Stream::Finalize() { Sha256Digest Result; NTSTATUS Status = BCryptFinishHash(static_cast(m_Ctx), Result.data(), static_cast(Result.size()), 0); ZEN_ASSERT(NT_SUCCESS(Status)); return Result; } Sha256Digest ComputeHmacSha256(const void* Key, size_t KeySize, const void* Data, size_t DataSize) { return BcryptHmac(GetBcryptHandles().HmacSha256, Key, KeySize, Data, DataSize); } #endif Sha256Digest ComputeSha256(std::string_view Data) { return ComputeSha256(Data.data(), Data.size()); } Sha256Digest ComputeHmacSha256(const Sha256Digest& Key, std::string_view Data) { return ComputeHmacSha256(Key.data(), Key.size(), Data.data(), Data.size()); } std::string Sha256ToHex(const Sha256Digest& Digest) { std::string Result; Result.reserve(64); for (uint8_t Byte : Digest) { fmt::format_to(std::back_inserter(Result), "{:02x}", Byte); } return Result; } void SecureZeroSecret(void* Data, size_t Size) { #if ZEN_PLATFORM_WINDOWS SecureZeroMemory(Data, Size); #elif ZEN_PLATFORM_LINUX explicit_bzero(Data, Size); #else // Portable fallback: volatile pointer prevents the compiler from optimizing away the memset static void* (*const volatile VolatileMemset)(void*, int, size_t) = memset; VolatileMemset(Data, 0, Size); #endif } ////////////////////////////////////////////////////////////////////////// // SigV4 signing namespace { std::string GetDateStamp(std::string_view AmzDate) { // AmzDate is "YYYYMMDDTHHMMSSZ", date stamp is first 8 chars return std::string(AmzDate.substr(0, 8)); } } // namespace std::string GetAmzTimestamp() { auto Now = std::chrono::system_clock::now(); std::time_t NowTime = std::chrono::system_clock::to_time_t(Now); struct tm Tm; #if ZEN_PLATFORM_WINDOWS gmtime_s(&Tm, &NowTime); #else gmtime_r(&NowTime, &Tm); #endif char Buf[32]; std::strftime(Buf, sizeof(Buf), "%Y%m%dT%H%M%SZ", &Tm); return std::string(Buf); } std::string AwsUriEncode(std::string_view Input, bool EncodeSlash) { static constexpr char kHex[] = "0123456789ABCDEF"; ExtendableStringBuilder<256> Result; for (char C : Input) { if ((C >= 'A' && C <= 'Z') || (C >= 'a' && C <= 'z') || (C >= '0' && C <= '9') || C == '_' || C == '-' || C == '~' || C == '.') { Result.Append(C); } else if (C == '/' && !EncodeSlash) { Result.Append(C); } else { // Hand-rolled hex encode; avoids per-char fmt::format std::string alloc // inside a loop that runs once per non-unreserved input character. const uint8_t Byte = static_cast(C); const char Encoded[3] = {'%', kHex[Byte >> 4], kHex[Byte & 0x0F]}; Result.Append(std::string_view(Encoded, 3)); } } return std::string(Result.ToView()); } std::string BuildCanonicalQueryString(std::vector> Parameters) { if (Parameters.empty()) { return {}; } // Sort by key name, then by value (as required by SigV4) std::sort(Parameters.begin(), Parameters.end()); ExtendableStringBuilder<512> Result; for (size_t i = 0; i < Parameters.size(); ++i) { if (i > 0) { Result.Append('&'); } Result.Append(AwsUriEncode(Parameters[i].first)); Result.Append('='); Result.Append(AwsUriEncode(Parameters[i].second)); } return std::string(Result.ToView()); } SigV4SignedHeaders SignRequestV4(const SigV4Credentials& Credentials, std::string_view Method, std::string_view Url, std::string_view CanonicalQueryString, std::string_view Region, std::string_view Service, std::string_view AmzDate, const std::vector>& Headers, std::string_view PayloadHash, const Sha256Digest* SigningKeyPtr) { SigV4SignedHeaders Result; Result.AmzDate = std::string(AmzDate); Result.PayloadHash = std::string(PayloadHash); std::string DateStamp = GetDateStamp(Result.AmzDate); // Step 1: canonical request. std::string CanonicalUri = AwsUriEncode(Url, false); // Headers assumed pre-sorted by lowercase name. ExtendableStringBuilder<512> CanonicalHeadersSb; ExtendableStringBuilder<256> SignedHeadersSb; for (size_t i = 0; i < Headers.size(); ++i) { CanonicalHeadersSb.Append(Headers[i].first); CanonicalHeadersSb.Append(':'); CanonicalHeadersSb.Append(Headers[i].second); CanonicalHeadersSb.Append('\n'); if (i > 0) { SignedHeadersSb.Append(';'); } SignedHeadersSb.Append(Headers[i].first); } std::string_view SignedHeaders = SignedHeadersSb.ToView(); ExtendableStringBuilder<2048> CanonicalRequestSb; CanonicalRequestSb.Append(Method); CanonicalRequestSb.Append('\n'); CanonicalRequestSb.Append(CanonicalUri); CanonicalRequestSb.Append('\n'); CanonicalRequestSb.Append(CanonicalQueryString); CanonicalRequestSb.Append('\n'); CanonicalRequestSb.Append(CanonicalHeadersSb.ToView()); CanonicalRequestSb.Append('\n'); CanonicalRequestSb.Append(SignedHeaders); CanonicalRequestSb.Append('\n'); CanonicalRequestSb.Append(PayloadHash); std::string_view CanonicalRequest = CanonicalRequestSb.ToView(); // Step 2: string-to-sign. ExtendableStringBuilder<128> CredentialScopeSb; CredentialScopeSb.Append(DateStamp); CredentialScopeSb.Append('/'); CredentialScopeSb.Append(Region); CredentialScopeSb.Append('/'); CredentialScopeSb.Append(Service); CredentialScopeSb.Append("/aws4_request"); std::string_view CredentialScope = CredentialScopeSb.ToView(); Sha256Digest CanonicalRequestHash = ComputeSha256(CanonicalRequest); std::string CanonicalRequestHex = Sha256ToHex(CanonicalRequestHash); ExtendableStringBuilder<256> StringToSignSb; StringToSignSb.Append("AWS4-HMAC-SHA256\n"); StringToSignSb.Append(Result.AmzDate); StringToSignSb.Append('\n'); StringToSignSb.Append(CredentialScope); StringToSignSb.Append('\n'); StringToSignSb.Append(CanonicalRequestHex); std::string_view StringToSign = StringToSignSb.ToView(); // Step 3: derive signing key (kDate -> kRegion -> kService -> kSigning HMAC chain). Sha256Digest DerivedSigningKey; if (!SigningKeyPtr) { std::string SecretPrefix = fmt::format("AWS4{}", Credentials.SecretAccessKey); Sha256Digest DateKey = ComputeHmacSha256(SecretPrefix.data(), SecretPrefix.size(), DateStamp.data(), DateStamp.size()); SecureZeroSecret(SecretPrefix.data(), SecretPrefix.size()); Sha256Digest RegionKey = ComputeHmacSha256(DateKey, Region); Sha256Digest ServiceKey = ComputeHmacSha256(RegionKey, Service); DerivedSigningKey = ComputeHmacSha256(ServiceKey, "aws4_request"); SigningKeyPtr = &DerivedSigningKey; } // Step 4: signature. Sha256Digest Signature = ComputeHmacSha256(*SigningKeyPtr, StringToSign); std::string SignatureHex = Sha256ToHex(Signature); // Step 5: Authorization header. Result.Authorization = fmt::format("AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}", Credentials.AccessKeyId, CredentialScope, SignedHeaders, SignatureHex); return Result; } std::string GeneratePresignedUrl(const SigV4Credentials& Credentials, std::string_view Method, std::string_view Scheme, std::string_view Host, std::string_view Path, std::string_view Region, std::string_view Service, std::chrono::seconds ExpiresIn, const std::vector>& ExtraQueryParams) { // Pre-signed URLs use query string authentication: // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html std::string AmzDate = GetAmzTimestamp(); std::string DateStamp = GetDateStamp(AmzDate); std::string CredentialScope = fmt::format("{}/{}/{}/aws4_request", DateStamp, Region, Service); std::string Credential = fmt::format("{}/{}", Credentials.AccessKeyId, CredentialScope); // The only signed header for pre-signed URLs is "host" constexpr std::string_view SignedHeaders = "host"; // Build query parameters that will be part of the canonical request. // These are the auth params (minus X-Amz-Signature which is added after signing). std::vector> QueryParams = ExtraQueryParams; QueryParams.emplace_back("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); QueryParams.emplace_back("X-Amz-Credential", Credential); QueryParams.emplace_back("X-Amz-Date", AmzDate); QueryParams.emplace_back("X-Amz-Expires", fmt::format("{}", ExpiresIn.count())); if (!Credentials.SessionToken.empty()) { QueryParams.emplace_back("X-Amz-Security-Token", Credentials.SessionToken); } QueryParams.emplace_back("X-Amz-SignedHeaders", std::string(SignedHeaders)); std::string CanonicalQueryString = BuildCanonicalQueryString(QueryParams); std::string CanonicalUri = AwsUriEncode(Path, false); // For pre-signed URLs, the payload is always UNSIGNED-PAYLOAD constexpr std::string_view PayloadHash = "UNSIGNED-PAYLOAD"; // Build the canonical request // Only "host" is in the canonical headers for pre-signed URLs std::string CanonicalHeaders = fmt::format("host:{}\n", Host); std::string CanonicalRequest = fmt::format("{}\n{}\n{}\n{}\n{}\n{}", Method, CanonicalUri, CanonicalQueryString, CanonicalHeaders, SignedHeaders, PayloadHash); // Create the string to sign Sha256Digest CanonicalRequestHash = ComputeSha256(CanonicalRequest); std::string CanonicalRequestHex = Sha256ToHex(CanonicalRequestHash); std::string StringToSign = fmt::format("AWS4-HMAC-SHA256\n{}\n{}\n{}", AmzDate, CredentialScope, CanonicalRequestHex); // Calculate the signing key std::string SecretPrefix = fmt::format("AWS4{}", Credentials.SecretAccessKey); Sha256Digest DateKey = ComputeHmacSha256(SecretPrefix.data(), SecretPrefix.size(), DateStamp.data(), DateStamp.size()); SecureZeroSecret(SecretPrefix.data(), SecretPrefix.size()); Sha256Digest RegionKey = ComputeHmacSha256(DateKey, Region); Sha256Digest ServiceKey = ComputeHmacSha256(RegionKey, Service); Sha256Digest SigningKey = ComputeHmacSha256(ServiceKey, "aws4_request"); // Calculate the signature std::string SignatureHex = Sha256ToHex(ComputeHmacSha256(SigningKey, StringToSign)); // Build the final URL (use the URI-encoded path so special characters are properly escaped) return fmt::format("{}://{}{}?{}&X-Amz-Signature={}", Scheme, Host, CanonicalUri, CanonicalQueryString, SignatureHex); } ////////////////////////////////////////////////////////////////////////// // Tests #if ZEN_WITH_TESTS void sigv4_forcelink() { } TEST_SUITE_BEGIN("util.cloud.sigv4"); TEST_CASE("sigv4.sha256") { // Test with known test vector (empty string) Sha256Digest Empty = ComputeSha256("", 0); std::string Hex = Sha256ToHex(Empty); CHECK(Hex == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); // Test with "hello" Sha256Digest Hello = ComputeSha256("hello"); std::string HelloHex = Sha256ToHex(Hello); CHECK(HelloHex == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"); } TEST_CASE("sigv4.sha256stream.matches_oneshot") { // Empty input. { Sha256Stream S; Sha256Digest Streamed = S.Finalize(); Sha256Digest OneShot = ComputeSha256("", 0); CHECK(Streamed == OneShot); } // Single update. { Sha256Stream S; S.Update("hello", 5); Sha256Digest Streamed = S.Finalize(); Sha256Digest OneShot = ComputeSha256("hello"); CHECK(Streamed == OneShot); } // Multi-chunk update; result must equal one-shot over concatenated bytes. { const std::string Whole = "the quick brown fox jumps over the lazy dog"; Sha256Stream S; S.Update(Whole.data(), 4); S.Update(Whole.data() + 4, 16); S.Update(Whole.data() + 20, Whole.size() - 20); Sha256Digest Streamed = S.Finalize(); Sha256Digest OneShot = ComputeSha256(Whole.data(), Whole.size()); CHECK(Streamed == OneShot); } // Large input fed in 256 KiB chunks (matches PutMedium hash pass shape). { std::vector Big(1u * 1024u * 1024u + 7u); for (size_t I = 0; I < Big.size(); ++I) { Big[I] = static_cast((I * 31u + 11u) & 0xFF); } Sha256Stream S; constexpr size_t kChunk = 256u * 1024u; size_t Off = 0; while (Off < Big.size()) { const size_t Take = std::min(kChunk, Big.size() - Off); S.Update(Big.data() + Off, Take); Off += Take; } Sha256Digest Streamed = S.Finalize(); Sha256Digest OneShot = ComputeSha256(Big.data(), Big.size()); CHECK(Streamed == OneShot); } // Move construct + finalize on the moved-to instance. { Sha256Stream A; A.Update("abc", 3); Sha256Stream B(std::move(A)); B.Update("def", 3); Sha256Digest Result = B.Finalize(); CHECK(Result == ComputeSha256("abcdef")); } } TEST_CASE("sigv4.hmac_sha256") { // RFC 4231 Test Case 2 std::string_view Key = "Jefe"; std::string_view Data = "what do ya want for nothing?"; Sha256Digest Result = ComputeHmacSha256(Key.data(), Key.size(), Data.data(), Data.size()); std::string Hex = Sha256ToHex(Result); CHECK(Hex == "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"); } TEST_CASE("sigv4.signing") { // Based on the AWS SigV4 test suite example // https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html SigV4Credentials Creds; Creds.AccessKeyId = "AKIDEXAMPLE"; Creds.SecretAccessKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; // We can't test with a fixed timestamp since SignRequestV4 uses current time, // but we can verify the crypto primitives produce correct results by testing // the signing key derivation manually. // Test signing key derivation: HMAC chain for "20150830" / "us-east-1" / "iam" std::string SecretPrefix = "AWS4wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; Sha256Digest DateKey = ComputeHmacSha256(SecretPrefix.data(), SecretPrefix.size(), "20150830", 8); Sha256Digest RegionKey = ComputeHmacSha256(DateKey, "us-east-1"); Sha256Digest ServiceKey = ComputeHmacSha256(RegionKey, "iam"); Sha256Digest SigningKey = ComputeHmacSha256(ServiceKey, "aws4_request"); std::string SigningKeyHex = Sha256ToHex(SigningKey); CHECK(SigningKeyHex == "c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9"); } TEST_SUITE_END(); #endif } // namespace zen