一、描述
QDtls 类可用于使用用户数据报协议 (UDP) 与网络对等点建立安全连接。
基于基本无连接 UDP 的 DTLS 连接意味着两个对等方首先必须通过调用 doHandshake() 成功完成 TLS 握手。握手完成后,可以使用 writeDatagramEncrypted() 将加密的数据报发送到对等方。来自对等方的加密数据报可以通过decryptDatagram() 解密。
QDtls 旨在与 QUdpSocket 一起使用。由于 QUdpSocket 可以接收来自不同对等方的数据报,因此应用程序必须实现多路分解,将来自不同对等方的数据报转发到其对应的 QDtls 实例。网络对等体与其 QDtls 对象之间的关联可以使用对等体的地址和端口号来建立。在开始握手之前,应用程序必须使用 setPeer() 设置对等方的地址和端口号。
QDtls 不从 QUdpSocket 读取数据报,这应该由应用程序完成,例如在QUdpSocket::readyRead() 信号的槽函数中读取数据报。然后,这些数据报必须由 QDtls 处理。
通常,在握手阶段,双方将接收和发送几个数据报。在读取数据报时,服务器和客户端必须将这些数据报传递给 doHandshake(),直到发现某些错误或 handshakeState() 返回 HandshakeComplete:
// 客户端发起握手:
QUdpSocket clientSocket;
QDtls clientDtls;
clientDtls.setPeer(address, port, peerName);
clientDtls.doHandshake(&clientSocket);
// 接受传入连接的服务器; 地址、端口、clientHello 由 QUdpSocket::readDatagram() 读取:
QByteArray clientHello(serverSocket.pendingDatagramSize(), Qt::Uninitialized);
QHostAddress address;
quin16 port = {};
serverSocket.readDatagram(clientHello.data(), clientHello.size(), &address, &port);
QDtls serverDtls;
serverDtls.setPeer(address, port);
serverDtls.doHandshake(&serverSocket, clientHello);
// 握手完成,对于服务器和客户端:
void DtlsConnection::continueHandshake(const QByteArray &datagram)
{
if (dtls.doHandshake(&udpSocket, datagram))
{
// Check handshake status:
if (dtls.handshakeStatus() == QDlts::HandshakeComplete)
{
// 安全 DTLS 连接现已建立。
}
}
else
{
// Error handling.
}
}
对于服务器,对 doHandshake() 的第一次调用需要一个包含 ClientHello 消息的非空数据报。 如果服务器也部署了 QDtlsClientVerifier,那么第一条 ClientHello 消息应该是 QDtlsClientVerifier 验证过的消息。
如果在握手期间无法验证对等方的身份,应用程序必须检查 peerVerificationErrors() 返回的错误,然后通过调用 ignoreVerificationErrors() 忽略错误或通过调用 abortHandshake() 中止握手。 如果错误被忽略,则可以通过调用 resumeHandshake() 来恢复握手。
握手完成后,数据报可以安全地发送到网络对等点并从网络对等点接收:
// 发送加密数据报:
dtlsConnection.writeDatagramEncrypted(&clientSocket, "Hello DTLS server!");
// 解密:
QByteArray encryptedMessage(dgramSize);
socket.readDatagram(encryptedMessage.data(), dgramSize);
const QByteArray plainText = dtlsConnection.decryptDatagram(&socket, encryptedMessage);
可以使用 shutdown() 关闭 DTLS 连接。
DtlsClient::~DtlsClient()
{
clientDtls.shutdown(&clientSocket);
}
警告:如果打算稍后重新使用相同的端口号连接到服务器,建议在销毁客户端的 QDtls 对象之前调用 shutdown()。 否则,服务器可能会丢弃传入的 ClientHello 消息。
如果服务器不使用 QDtlsClientVerifier,它必须配置它的 QDtls 对象以禁用 cookie 验证过程:
QSslConfiguration config = QSslConfiguration::defaultDtlsConfiguration();
config.setDtlsCookieVerificationEnabled(false);
dtlsConnection.setDtlsConfiguration(config);
二、相关非成员
1、enum class QDtlsError:此枚举描述了 QDtlsClientVerifier 和 QDtls 类的对象可能遇到的一般错误和特定于 TLS 的错误。
- QDtls::QDtlsError::NoError:没有发生错误,上次操作成功。
- QDtls::QDtlsError::InvalidInputParameters:调用者提供的输入参数无效。
- QDtls::QDtlsError::InvalidOperation:在不允许的状态下尝试操作。
- QDtls::QDtlsError::UnderlyingSocketError:QUdpSocket::writeDatagram() 失败,QUdpSocket::error() 和 QUdpSocket::errorString() 可以提供更具体的信息。
- QDtls::QDtlsError::RemoteClosedConnectionError:收到 TLS 关闭警报消息。
- QDtls::QDtlsError::PeerVerificationError:在 TLS 握手期间无法验证对等方的身份。
- QDtls::QDtlsError::TlsInitializationError:初始化底层 TLS 后端时出错。
- QDtls::QDtlsError::TlsFatalError:TLS 握手期间发生了致命错误,而不是对等验证错误或 TLS 初始化错误。
- QDtls::QDtlsError::TlsNonFatalError:加密或解密数据报失败,非致命的,QDtls 可以在出现此错误后继续工作。
三、类型成员
1、enum QDtls::HandshakeState:此枚举描述了 QDtls 连接的 DTLS 握手的当前状态。
- HandshakeNotStarted:未开始连接。
- HandshakeInProgress:握手已启动,目前未发现任何错误。
- PeerVerificationFailed:无法建立对等方的身份。
- HandshakeComplete:握手成功完成并建立加密连接。
四、成员函数
1、[signal] void handshakeTimeout()
握手超时信号。数据包丢失可能导致握手阶段超时。在这种情况下,QDtls 发出此信号。
调用 handleTimeout() 重传握手消息:
DtlsClient::DtlsClient()
{
connect(&clientDtls, &QDtls::handshakeTimeout, this, &DtlsClient::handleTimeout);
}
void DtlsClient::handleTimeout()
{
clientDtls.handleTimeout(&clientSocket);
}
2、[signal] void pskRequired(QSslPreSharedKeyAuthenticator *authenticator)
QDtls 在协商 PSK 密码套件时发出此信号。
使用 PSK 时,客户端必须向服务器发送有效身份和有效的预共享密钥,以便 TLS 握手继续。 应用程序可以在连接到这个信号的槽函数中提供相应信息,根据需要填充传递的验证器对象。
注意:忽略此信号,或未能提供所需的凭据,将导致握手失败,从而中止连接。
注意:authenticator 对象归 QDtls 所有,不得被应用程序删除。
3、bool abortHandshake(QUdpSocket *socket)
中止正在进行的握手。成功则返回 true,否则设置一个合适的错误并返回 false。
4、QByteArray decryptDatagram(QUdpSocket *socket, const QByteArray &dgram)
解密 dgram 并以纯文本形式返回其内容。
5、bool doHandshake(QUdpSocket *socket, const QByteArray &dgram = {})
开始或继续 DTLS 握手。开始服务器端 DTLS 握手时,dgram 必须包含从 QUdpSocket 读取的初始 ClientHello 消息。 未发现错误返回 true。
6、bool handleTimeout(QUdpSocket *socket)
如果握手期间发生超时,则会发出 handshakeTimeout() 信号,这时可以调用此函数来重新传输握手消息。
7、void ignoreVerificationErrors(const QVector &errorsToIgnore)
忽略在 errorsToIgnore 中给出的错误。
也可以在 doHandshake() 遇到 QDtlsError::PeerVerificationError 错误后调用此函数,然后通过调用 resumeHandshake() 恢复握手
例如,如果想连接到使用自签名证书的服务器,可以考虑使用以下代码段:
QList<QSslCertificate> cert = QSslCertificate::fromPath(QLatin1String("server-certificate.pem"));
QSslError error(QSslError::SelfSignedCertificate, cert.at(0));
QList<QSslError> expectedSslErrors;
expectedSslErrors.append(error);
QDtls dtls;
dtls.ignoreVerificationErrors(expectedSslErrors);
dtls.doHandshake(udpSocket);
8、bool isConnectionEncrypted()
DTLS 是否握手成功完成。
9、QVectorpeerVerificationErrors()
返回对等体身份验证时发现的错误。如果想在发生错误的情况下继续连接,必须调用 ignoreVerificationErrors()。
10、bool resumeHandshake(QUdpSocket *socket)
如果在握手期间忽略对等身份验证错误,此函数将恢复并完成握手并返回 true。
11、QSslCipher sessionCipher()
返回此连接使用的加密密码,如果连接未加密,则返回空密码。会话的密码是在握手阶段选择的。 密码用于加密和解密数据。
QSslConfiguration 提供了用于设置密码的有序列表的功能,握手阶段最终将从中选择会话密码。
12、QSsl::SslProtocol sessionProtocol()
返回此连接使用的 DTLS 协议版本,如果连接尚未加密,则返回 UnknownProtocol。在握手阶段选择连接的协议。
setDtlsConfiguration() 可以在握手开始之前设置首选版本。
13、bool setCookieGeneratorParameters(const QDtls::GeneratorParameters ¶ms)
设置密码哈希算法和来自 params 的秘密。此函数仅用于服务器端 QDtls 连接。
14、bool setDtlsConfiguration(const QSslConfiguration &configuration)
设置连接的 TLS 配置,如果成功则返回 true。这个函数必须在握手开始前调用。
15、bool setPeer(const QHostAddress &address, quint16 port, const QString &verificationName = {})
设置对等方的地址、端口和主机名。 地址不得为空、多播地址、广播地址。verifyName 是用于证书验证的主机名。
16、bool setPeerVerificationName(const QString &name)
设置将用于证书验证的主机名。
17、bool shutdown(QUdpSocket *socket)
发送加密关闭警报消息并关闭 DTLS 连接。握手状态更改为 QDtls::HandshakeNotStarted。
18、QSslSocket::SslMode sslMode()
为服务器端连接返回 QSslSocket::SslServerMode,
为客户端返回 QSslSocket::SslClientMode。
19、qint64 writeDatagramEncrypted(QUdpSocket *socket, const QByteArray &dgram)
加密 dgram 并将加密数据写入socket。 返回写入的字节数,如果出错则返回 -1。