Files
UnrealEngine/Engine/Source/Runtime/Online/SSL/Private/SslCertificateManager.cpp
2025-05-18 13:04:45 +08:00

474 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SslCertificateManager.h"
#include "Ssl.h"
#include "SslError.h"
#include "Misc/ConfigCacheIni.h"
#include "HAL/PlatformFile.h"
#include "HAL/FileManager.h"
#include "Templates/UniquePtr.h"
#include "Misc/Base64.h"
#include "Misc/CommandLine.h"
#include <Algo/Count.h>
FSslCertificateDelegates::FVerifySslCertificates FSslCertificateDelegates::VerifySslCertificates;
#if WITH_SSL
#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
#endif
#include <openssl/ssl.h>
#include <openssl/x509v3.h>
#if PLATFORM_WINDOWS
#include "Windows/HideWindowsPlatformTypes.h"
#endif
namespace
{
FString GetCertificateName(X509* const Certificate)
{
char StaticBuffer[2048];
// We do not have to free the return value of get_subject_name
X509_NAME_oneline(X509_get_subject_name(Certificate), StaticBuffer, sizeof(StaticBuffer));
return FString(ANSI_TO_TCHAR(StaticBuffer));
}
FString GetCertificateIssuer(X509* const Certificate)
{
char StaticBuffer[2048];
// We do not have to free the return value of get_subject_name
X509_NAME_oneline(X509_get_issuer_name(Certificate), StaticBuffer, sizeof(StaticBuffer));
return FString(ANSI_TO_TCHAR(StaticBuffer));
}
}
void FSslCertificateManager::AddCertificatesToSslContext(SSL_CTX* SslContextPtr) const
{
X509_STORE* CertStore = SSL_CTX_get_cert_store(SslContextPtr);
for (int i = 0; i < RootCertificateArray.Num(); ++i)
{
if (X509_STORE_add_cert(CertStore, RootCertificateArray[i]) == 0)
{
UE_LOG(LogSsl, Log, TEXT("Unable to add certificate: %s"), *GetSslErrorString());
}
}
}
bool FSslCertificateManager::HasCertificatesAvailable() const
{
return RootCertificateArray.Num() > 0;
}
void FSslCertificateManager::ClearAllPinnedPublicKeys()
{
PinnedPublicKeys.Empty();
}
bool FSslCertificateManager::HasPinnedPublicKeys() const
{
return PinnedPublicKeys.Num() > 0;
}
// Compare function to order domains by exact matches, then from most specific to least specific subdomain matches
// For example: { "a.b.c.d", ".b.c.d", ".c.d", ".d" }
static bool DomainLessThan(const FString& DomainA, const FString& DomainB)
{
const bool bDomainAIncludesSubdomains = DomainA[0] == TEXT('.');
const bool bDomainBIncludesSubdomains = DomainB[0] == TEXT('.');
if (bDomainAIncludesSubdomains == bDomainBIncludesSubdomains)
{
if (bDomainAIncludesSubdomains)
{
// both start with '.', sort by number of '.'s
const SIZE_T DomainAPeriods = Algo::Count(DomainA, TEXT('.'));
const SIZE_T DomainBPeriods = Algo::Count(DomainB, TEXT('.'));
if (DomainAPeriods == DomainBPeriods)
{
// sort alphabetically
return DomainA < DomainB;
}
else
{
// sort from most specific to least specific
return DomainAPeriods > DomainBPeriods;
}
}
else
{
// sort alphabetically
return DomainA < DomainB;
}
}
else
{
// exact matches come first
return bDomainBIncludesSubdomains;
}
}
bool FSslCertificateManager::IsDomainPinned(const FString& Domain)
{
bool bWasDomainFound = false;
FString DomainWithoutPort = Domain;
int PortStart = Domain.Find(TEXT(":"), ESearchCase::IgnoreCase, ESearchDir::FromEnd);
if (PortStart >= 0)
{
int PortLength = DomainWithoutPort.Len() - PortStart;
DomainWithoutPort.RemoveAt(PortStart, PortLength);
}
const TArray<TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>>>* PinnedKeys = nullptr;
for (const TPair<FString, TArray<TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>>>>& PinnedKeyPair : PinnedPublicKeys)
{
const FString& PinnedDomain = PinnedKeyPair.Key;
if ((PinnedDomain[0] == TEXT('.') && DomainWithoutPort.EndsWith(PinnedDomain))
|| DomainWithoutPort == PinnedDomain)
{
bWasDomainFound = true;
break;
}
}
return bWasDomainFound;
}
void FSslCertificateManager::SetPinnedPublicKeys(const FString& Domain, const FString& PinnedKeyDigests)
{
if (Domain.Len() == 0)
{
return;
}
if (PinnedKeyDigests.IsEmpty())
{
PinnedPublicKeys.RemoveAll([&Domain](const TPair<FString, TArray<TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>>>>& Pair) { return Pair.Key == Domain; });
}
else
{
int32 FoundIndex = INDEX_NONE;
for (int Index = 0; Index < PinnedPublicKeys.Num(); ++Index)
{
const FString& ElementDomain = PinnedPublicKeys[Index].Key;
if (ElementDomain == Domain)
{
FoundIndex = Index;
break;
}
else if (DomainLessThan(Domain, ElementDomain))
{
FoundIndex = Index;
PinnedPublicKeys.EmplaceAt(Index, Domain, TArray<TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>>>());
break;
}
}
if (FoundIndex == INDEX_NONE)
{
FoundIndex = PinnedPublicKeys.Emplace(Domain, TArray<TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>>>());
}
TArray<TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>>>& PinnedDigests = PinnedPublicKeys[FoundIndex].Value;
PinnedDigests.Reset();
TArray<FString> Digests;
PinnedKeyDigests.ParseIntoArray(Digests, TEXT(";"));
for (const FString& Digest : Digests)
{
if (FBase64::GetDecodedDataSize(Digest) == PUBLIC_KEY_DIGEST_SIZE)
{
TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>> DecodedDigest;
DecodedDigest.AddUninitialized(PUBLIC_KEY_DIGEST_SIZE);
if (FBase64::Decode(*Digest, Digest.Len(), DecodedDigest.GetData()))
{
PinnedDigests.Add(DecodedDigest);
}
}
}
}
}
bool FSslCertificateManager::VerifySslCertificates(X509_STORE_CTX* Context, const FString& Domain) const
{
#if !UE_BUILD_SHIPPING || WITH_SERVER_CODE
static const bool bPinningDisabled = FParse::Param(FCommandLine::Get(), TEXT("DisableSSLCertificatePinning"));
if (bPinningDisabled)
{
return true;
}
#endif
STACK_OF(X509)* Chain = X509_STORE_CTX_get_chain(Context);
const int NumCertsInChain = sk_X509_num(Chain);
if (NumCertsInChain <= 0)
{
return false;
}
TArray<TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>>> CertDigests;
TArray<FSslCertificateDelegates::FCertInfo> CertInfoList;
const bool bCollectCertInfo = FSslCertificateDelegates::VerifySslCertificates.IsBound();
for (int CertIndex = 0; CertIndex < NumCertsInChain; ++CertIndex)
{
X509* Certificate = sk_X509_value(Chain, CertIndex);
int Length = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(Certificate), nullptr);
if (Length <= 0)
{
// no key
continue;
}
TArray<uint8> PubKey;
PubKey.AddUninitialized(Length);
uint8* PubKeyPtr = PubKey.GetData();
i2d_X509_PUBKEY(X509_get_X509_PUBKEY(Certificate), &PubKeyPtr);
TArray<uint8, TFixedAllocator<SHA256_DIGEST_LENGTH>> Digest;
Digest.AddUninitialized(SHA256_DIGEST_LENGTH);
SHA256_CTX ShaContext;
SHA256_Init(&ShaContext);
SHA256_Update(&ShaContext, PubKey.GetData(), PubKey.Num());
SHA256_Final(Digest.GetData(), &ShaContext);
CertDigests.Add(Digest);
if (bCollectCertInfo)
{
FSslCertificateDelegates::FCertInfo CertInfo;
CertInfo.KeyDigest = Digest;
CertInfo.Issuer = GetCertificateIssuer(Certificate);
CertInfo.Subject = GetCertificateName(Certificate);
const EVP_MD* CertDigest = EVP_get_digestbyname("sha1");
if (CertDigest)
{
unsigned int DummySize = 0;
CertInfo.Thumbprint.AddZeroed(FSslCertificateDelegates::FCertInfo::CERT_DIGEST_SIZE);
X509_digest(Certificate, CertDigest, CertInfo.Thumbprint.GetData(), &DummySize);
}
CertInfoList.Add(CertInfo);
}
}
bool bFoundMatch = false;
bool bFoundMatchDelegate = FSslCertificateDelegates::VerifySslCertificates.IsBound() ? FSslCertificateDelegates::VerifySslCertificates.Execute(Domain, CertInfoList) : true;
if (bFoundMatchDelegate)
{
bFoundMatch = VerifySslCertificates(CertDigests, Domain);
}
if (!bFoundMatch)
{
X509_STORE_CTX_set_error(Context, X509_V_ERR_CERT_UNTRUSTED);
}
return bFoundMatch;
}
bool FSslCertificateManager::VerifySslCertificates(TArray<TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>>>& Digests, const FString& Domain) const
{
#if !UE_BUILD_SHIPPING || WITH_SERVER_CODE
static const bool bPinningDisabled = FParse::Param(FCommandLine::Get(), TEXT("DisableSSLCertificatePinning"));
if (bPinningDisabled)
{
return true;
}
#endif
const TArray<TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>>>* PinnedKeys = nullptr;
for (const TPair<FString, TArray<TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>>>>& PinnedKeyPair : PinnedPublicKeys)
{
const FString& PinnedDomain = PinnedKeyPair.Key;
if ((PinnedDomain[0] == TEXT('.') && Domain.EndsWith(PinnedDomain))
|| Domain == PinnedDomain)
{
PinnedKeys = &PinnedKeyPair.Value;
break;
}
}
if (!PinnedKeys)
{
// No keys pinned for this domain
UE_LOG(LogSsl, Verbose, TEXT("no pinned key digests found for domain '%s'"), *Domain);
return true;
}
bool bFoundMatch = false;
for (const TArray<uint8, TFixedAllocator<PUBLIC_KEY_DIGEST_SIZE>>& CurrentDigest: Digests)
{
UE_LOG(LogSsl, VeryVerbose, TEXT("checking digest. Base64: '%s'"), *FBase64::Encode(CurrentDigest.GetData(), CurrentDigest.Num()));
if (PinnedKeys->Contains(CurrentDigest))
{
UE_LOG(LogSsl, Verbose, TEXT("found public key digest in request that matches a pinned key for '%s'"), *Domain);
bFoundMatch = true;
break;
}
}
return bFoundMatch;
}
void FSslCertificateManager::BuildRootCertificateArray()
{
FString CertificateBundlePath;
#if !UE_BUILD_SHIPPING
FString OverrideCertificateBundlePath;
if (GConfig->GetString(TEXT("SSL"), TEXT("OverrideCertificateBundlePath"), OverrideCertificateBundlePath, GEngineIni) && OverrideCertificateBundlePath.Len() > 0)
{
if (FPaths::FileExists(OverrideCertificateBundlePath))
{
CertificateBundlePath = OverrideCertificateBundlePath;
}
}
#endif
if (CertificateBundlePath.IsEmpty())
{
const FString PerPlatformBundlePath = FString::Printf(TEXT("Certificates/%s/cacert.pem"), ANSI_TO_TCHAR(FPlatformProperties::IniPlatformName()));
const FString SearchPaths[]
{
FPaths::ProjectContentDir() + PerPlatformBundlePath,
FPaths::ProjectContentDir() + TEXT("Certificates/cacert.pem"),
FPaths::EngineContentDir() + TEXT("Certificates/ThirdParty/cacert.pem")
};
for (const FString& SearchPath : SearchPaths)
{
if (FPaths::FileExists(SearchPath))
{
CertificateBundlePath = SearchPath;
break;
}
}
}
if (!CertificateBundlePath.IsEmpty())
{
AddPEMFileToRootCertificateArray(CertificateBundlePath);
}
FString DebuggingCertificatePath;
if (GConfig->GetString(TEXT("SSL"), TEXT("DebuggingCertificatePath"), DebuggingCertificatePath, GEngineIni) && DebuggingCertificatePath.Len() > 0)
{
if (FPaths::FileExists(DebuggingCertificatePath))
{
TUniquePtr<FArchive> DebuggingCertificateArchive(IFileManager::Get().CreateFileReader(*DebuggingCertificatePath, 0));
int64 CertificateBufferSize = DebuggingCertificateArchive->TotalSize();
char* CertificateBuffer = new char[CertificateBufferSize + 1];
DebuggingCertificateArchive->Serialize(CertificateBuffer, CertificateBufferSize);
CertificateBuffer[CertificateBufferSize] = '\0';
BIO* CertificateBio = BIO_new_mem_buf(CertificateBuffer, -1);
X509* Certificate = PEM_read_bio_X509(CertificateBio, NULL, 0, NULL);
if (Certificate)
{
AddCertificateToRootCertificateArray(Certificate);
}
else
{
UE_LOG(LogSsl, Warning, TEXT("Error loading debugging certificate: %s"), *GetSslErrorString());
}
BIO_free(CertificateBio);
delete[] CertificateBuffer;
CertificateBuffer = nullptr;
}
}
}
void FSslCertificateManager::EmptyRootCertificateArray()
{
for (int i = 0; i < RootCertificateArray.Num(); ++i)
{
X509_free(RootCertificateArray[i]);
}
RootCertificateArray.Reset();
}
void FSslCertificateManager::AddPEMFileToRootCertificateArray(const FString& Path)
{
int64 CertificateBundleBufferSize = 0;
TUniquePtr<char[]> CertificateBundleBuffer;
if (TUniquePtr<FArchive> CertificateBundleArchive = TUniquePtr<FArchive>(IFileManager::Get().CreateFileReader(*Path, 0)))
{
CertificateBundleBufferSize = CertificateBundleArchive->TotalSize();
CertificateBundleBuffer.Reset(new char[CertificateBundleBufferSize + 1]);
CertificateBundleArchive->Serialize(CertificateBundleBuffer.Get(), CertificateBundleBufferSize);
CertificateBundleBuffer[CertificateBundleBufferSize] = '\0';
}
if (CertificateBundleBufferSize > 0 && CertificateBundleBuffer != nullptr)
{
static const char BeginCertificateString[] = "-----BEGIN CERTIFICATE-----";
static const char EndCertificateString[] = "-----END CERTIFICATE-----";
const char* FoundString = CertificateBundleBuffer.Get();
while (nullptr != (FoundString = FPlatformString::Strstr(FoundString, BeginCertificateString)))
{
const char* EndString = FPlatformString::Strstr(FoundString, EndCertificateString);
if (EndString != nullptr)
{
size_t LengthOfCertificateData = EndString - FoundString + sizeof(EndCertificateString);
BIO* CertificateBio = BIO_new_mem_buf(const_cast<char*>(FoundString), LengthOfCertificateData);
X509* Certificate = PEM_read_bio_X509(CertificateBio, NULL, 0, NULL);
if (Certificate)
{
AddCertificateToRootCertificateArray(Certificate);
}
else
{
UE_LOG(LogSsl, Log, TEXT("Error loading certificate from bundle: %s"), *GetSslErrorString());
}
BIO_free(CertificateBio);
}
FoundString = EndString;
}
}
}
void FSslCertificateManager::AddCertificateToRootCertificateArray(X509* Certificate)
{
bool bValidateRootCertificates = true;
GConfig->GetBool(TEXT("SSL"), TEXT("bValidateRootCertificates"), bValidateRootCertificates, GEngineIni);
if (bValidateRootCertificates)
{
ASN1_TIME* NotBefore = X509_get_notBefore(Certificate);
ASN1_TIME* NotAfter = X509_get_notAfter(Certificate);
if (X509_cmp_current_time(NotAfter) < 0)
{
UE_LOG(LogSsl, Log, TEXT("Ignoring expired certificate: %s"), *GetCertificateName(Certificate));
X509_free(Certificate);
return;
}
if (X509_cmp_current_time(NotBefore) > 0)
{
UE_LOG(LogSsl, Log, TEXT("Ignoring not yet valid certificate: %s"), *GetCertificateName(Certificate));
X509_free(Certificate);
return;
}
}
const bool bFound = RootCertificateArray.ContainsByPredicate(
[Certificate](X509* Other)
{
return X509_cmp(Other, Certificate) == 0;
});
if (bFound)
{
UE_LOG(LogSsl, VeryVerbose, TEXT("Ignoring duplicate certificate: %s"), *GetCertificateName(Certificate));
X509_free(Certificate);
}
else
{
UE_LOG(LogSsl, Verbose, TEXT("Adding certificate: %s"), *GetCertificateName(Certificate));
RootCertificateArray.Add(Certificate);
}
}
#endif // #if WITH_SSL