在Unreal引擎中应用TLS加密
在Unreal引擎中,可以通过DTLSHandlerComponent对网络流量进行加密。DTLSHandlerComponent是Unreal内置的插件,不过关于这个插件,网上的资料不多,我将自己摸索的过程整理成文,希望可以帮到更多的朋友。启用插件
在Plugins窗口搜索DTLS network packet handler,勾选Enabled。
可以看到,这个插件还处于Experimental状态。
配置EncryptionComponent
每一个UNetConnection,都对应一个PacketHandler,PacketHandler中可以注册任意数量的HandlerComponent,这些HandlerComponent,都可以对发送和接收到的packet进行加工(比如加密)。packet就像是一个包裹,沿着传送带,流经每一个HandlerComponent,进行加工和处理,最终发送/接收。
从PacketHandler::Initialize中我们可以看到,Unreal会从配置文件的PacketHandlerComponents区段,读取EncryptionComponent,并将其注册到PacketHandler中。
我们直接在DefaultEngine.ini中增加配置即可:
EncryptionComponent=DTLSHandlerComponent加入游戏的流程
在NetDriver.h中,有一段非常长的注释,详细介绍了Unreal如何处理联机游戏。
找到UWorld / UPendingNetGame / AGameModeBase Startup and Handshaking这一节,我们可以看到Client/Server是如何进行握手的,我大致翻译了一下,不过关于加密的流程没有记述,我用粗体补充了一下:
[*]客户端的UPendingNetGame::SendInitialJoin发送NMT_Hello,若URL包含非空的EncryptionToken参数,则会将其包含在NMT_Hello消息中
[*]服务端的UWorld::NotifyControlMessage收到NMT_Hello, 回调OnReceivedNetworkEncryptionToken,根据EncryptionToken生成EncryptionData,然后调用UNetConnection::SendChallengeControlMessage。
[*]若前一步设置了有效的EncryptionData,服务端的UNetConnection::SendClientEncryptionAck会发送NMT_EncryptionAck,并调用EnableEncryption开启加密。
[*]客户端的UPendingNetGame::NotifyControlMessage收到NMT_EncryptionAck,回调OnReceivedNetworkEncryptionAck,并设置好客户端需要用的EncryptionData,然后调用UPendingNetGame::FinalizeEncryptedConnection,如果设置了有效的EncryptionData,则会在客户端调用EnableEncryption开启加密。
[*]服务端发送NMT_Challenge,对客户端进行验证。
[*]客户端的UPendingNetGame::NotifyControlMessage收到NMT_Challenge,填充验证数据,并发送NMT_Login
[*]服务端的UWorld::NotifyControlMessage收到NMT_Login,验证数据,然后调用AGameModeBase::PreLogin。如果PreLogin无任何错误,服务端就会调用UWorld::WelcomePlayer,进一步调用AGameModeBase::GameWelcomePlayer并发送包含了地图信息的NMT_Welcome消息
[*]客户端的UPendingNetGame::NotifyControlMessage收到NMT_Welcome,读取地图信息,通过NMT_NetSpeed消息将客户端配置的网络速度告知服务端。
[*]服务端的UWorld::NotifyControlMessage收到NMT_NetSpeed,相应地调整该网络连接的速度。
[*]至此,握手完成。
以上述流程中,我们需要实现OnReceivedNetworkEncryptionToken、OnReceivedNetworkEncryptionAck,并且在URL中带上EncryptionToken即可。
加密方式
DTLSHandlerComponent的底层实现是openssl,它支持两种加密套件:PSK以及HIGH。
其中第一种加密套件PSK,即Pre-Shared Key,客户端和服务端使用预先协商好的密钥进行通信,因此不需要证书。PSK也是默认的加密套件。例如:客户端可以将Hash过的PSK通过EncryptionToken发送,服务端收到EncryptionToken后根据同样的算法,算出EncryptionToken。
HIGH加密套件,需要借助证书进行身份认证以及后续的密钥协商,也是本文主要讨论的一种。DTLSHandlerComponent对于证书形式的加密支持有一些限制:
[*]服务端需预先通过DTLSCertStore::CreateCert创建证书,不支持从外部导入
[*]客户端要求服务端发来的证书是自签名的
[*]客户端会验证服务端发来的证书的SHA256指纹,强制支持TLS Pinning
在不修改源代码的情况下,由于1、3的限制,我们必须在服务端创建好证书后,通过其他可信渠道将证书指纹通知到客户端,否则指纹校验会失败。2的限制,在游戏领域问题不大,就算改代码支持CA签名证书也不难。
代码实现
由于前一节尾提到的一些限制,以及其他实际需求,对这个插件做了如下定制:
[*]将CVarPreSharedKeys的默认值改为0,默认使用HIGH,而非PSK
[*]修改DTLSCertStore、DTLSCertificate,支持从外部导入证书和私玥
OnReceivedNetworkEncryptionToken、OnReceivedNetworkEncryptionAck这两个Delegate默认值尾UGameInstance上的同名函数,因此我们新增UEncryptedGameInstance,并在此文件中实现功能:
TSharedPtr<FDTLSCertificate> UEncryptedGameInstance::LoadCertificate(const char* CertificatePath, const char* PrivateKeyPath)
{
FILE* fp = nullptr;
X509* Certificate = nullptr;
EVP_PKEY* PrivateKey = nullptr;
bool Succeed = false;
// reading Certificate
fp = fopen(CertificatePath, &#34;r&#34;);
if (fp == nullptr)
goto cleanup;
Certificate = PEM_read_X509(fp, NULL, 0, NULL);
fclose(fp);
// reading PrivateKey
fp = fopen(PrivateKeyPath, &#34;r&#34;);
if (fp == nullptr)
goto cleanup;
PrivateKey = PEM_read_PrivateKey(fp, NULL, 0, NULL);
Succeed = true;
cleanup:
if (fp != nullptr) fclose(fp);
if (!Succeed)
{
if (Certificate != nullptr) X509_free(Certificate);
if (PrivateKey != nullptr) EVP_PKEY_free(PrivateKey);
return nullptr;
}
return MakeShared<FDTLSCertificate>(Certificate, PrivateKey);
}
/** Handle setting up encryption keys. Games that override this MUST call the delegate when their own (possibly async) processing is complete. */
void UEncryptedGameInstance::ReceivedNetworkEncryptionToken(const FString& EncryptionToken, const FOnEncryptionKeyResponse& Delegate)
{
#if WITH_SERVER_CODE
// Client will grab EncryptionToken from URL Option &#34;EncryptionToken=&#34;
TSharedPtr<FDTLSCertificate> Cert = FDTLSCertStore::Get().GetCert(CERT_ID);
if (!Cert.IsValid())
{
FEncryptionKeyResponse Response(EEncryptionResponse::Failure, TEXT(&#34;Certificate not found!&#34;));
Delegate.ExecuteIfBound(Response);
}
else
{
FEncryptionKeyResponse Response(EEncryptionResponse::Success, TEXT(&#34;Certificate setup correctly!&#34;));
Response.EncryptionData.Identifier = CERT_ID;
Delegate.ExecuteIfBound(Response);
}
#endif
}
/** Called when a client receives the EncryptionAck control message from the server, will generally enable encryption. */
void UEncryptedGameInstance::ReceivedNetworkEncryptionAck(const FOnEncryptionKeyResponse& Delegate)
{
FEncryptionKeyResponse Response(EEncryptionResponse::Success, TEXT(&#34;ReceivedNetworkEncryptionAck&#34;));
Response.EncryptionData.Fingerprint.SetNumUninitialized(FDTLSFingerprint::Length);
HexToBytes(FINGER_PRINT, Response.EncryptionData.Fingerprint.GetData());
Delegate.ExecuteIfBound(Response);
}
总结
至此,TLS加密就可以正常工作了。
经过这次的摸索,我对Unreal引擎进入房间的流程、以及加密协商的流程都有了简单的了解,并且对TLS加密的流程也有了初步的认识。
TLS加密只是从网络通信的层面加强了安全,客户端如果遭到篡改,很容易导致TLS加密失效。如果控制台变量net.AllowEncryption被修改为0,或者在发送URL的时候,将其中的EncryptionToken置空,那么加密通讯将会被禁用。因此有必要再对Unreal引擎的源码做一些调整,保证服务器能始终开启加密,迫使被篡改的客户端无法正常于服务器进行通讯。
页:
[1]