找回密码
 立即注册
查看: 327|回复: 0

在Unreal引擎中应用TLS加密

[复制链接]
发表于 2022-7-7 09:26 | 显示全部楼层 |阅读模式
在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中增加配置即可:
[PacketHandlerComponents]
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, "r");
        if (fp == nullptr)
                goto cleanup;
        Certificate = PEM_read_X509(fp, NULL, 0, NULL);
        fclose(fp);

        // reading PrivateKey
        fp = fopen(PrivateKeyPath, "r");
        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 "EncryptionToken="
        TSharedPtr<FDTLSCertificate> Cert = FDTLSCertStore::Get().GetCert(CERT_ID);
        if (!Cert.IsValid())
        {
                FEncryptionKeyResponse Response(EEncryptionResponse::Failure, TEXT("Certificate not found!"));
                Delegate.ExecuteIfBound(Response);
        }
        else
        {
                FEncryptionKeyResponse Response(EEncryptionResponse::Success, TEXT("Certificate setup correctly!"));
                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("ReceivedNetworkEncryptionAck"));
        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引擎的源码做一些调整,保证服务器能始终开启加密,迫使被篡改的客户端无法正常于服务器进行通讯。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 07:17 , Processed in 0.088051 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表