DTLSクライアント

この例は、クライアント側のDTLS接続の実装方法を示す。

Screenshot of the DTLS client example.

注: DTLSクライアントの例は、DTLSサーバーの例と一緒に実行することを想定しています。

DTLSクライアントの例では、1つまたは複数のDTLSサーバーに複数のDTLS接続を確立することができます。クライアント側のDTLS接続は、DtlsAssociationクラスによって実装される。このクラスはデータグラムの読み書きにQUdpSocket 、暗号化にQDtls

class DtlsAssociation : public QObject
{
    Q_OBJECT

public:
    DtlsAssociation(const QHostAddress &address, quint16 port,
                    const QString &connectionName);
    ~DtlsAssociation();
    void startHandshake();

signals:
    void errorMessage(const QString &message);
    void warningMessage(const QString &message);
    void infoMessage(const QString &message);
    void serverResponse(const QString &clientInfo, const QByteArray &datagraam,
                        const QByteArray &plainText);

private slots:
    void udpSocketConnected();
    void readyRead();
    void handshakeTimeout();
    void pskRequired(QSslPreSharedKeyAuthenticator *auth);
    void pingTimeout();

private:
    QString name;
    QUdpSocket socket;
    QDtls crypto;

    QTimer pingTimer;
    unsigned ping = 0;

    Q_DISABLE_COPY(DtlsAssociation)
};

コンストラクタは、新しいDTLS接続のための最小限のTLSコンフィギュレーションを設定し、サーバーのアドレスとポートを設定する:

    ...
auto configuration = QSslConfiguration::defaultDtlsConfiguration();
configuration.setPeerVerifyMode(QSslSocket::VerifyNone);
crypto.setPeer(address, port);
crypto.setDtlsConfiguration(configuration);
    ...

QDtls::handshakeTimeout() シグナルは handleTimeout() スロットに接続され、ハンドシェイク・フェーズ中のパケット損失と再送に対処する:

    ...
connect(&crypto, &QDtls::handshakeTimeout, this, &DtlsAssociation::handshakeTimeout);
    ...

サーバーからのデータグラムだけを確実に受信するために、UDPソケットをサーバーに接続する:

    ...
socket.connectToHost(address.toString(), port);
    ...

QUdpSocket::readyRead()シグナルはreadyRead()スロットに接続されている:

    ...
connect(&socket, &QUdpSocket::readyRead, this, &DtlsAssociation::readyRead);
    ...

サーバーへのセキュアな接続が確立されると、DtlsAssociationオブジェクトが、タイマーを使ってサーバーに短いpingメッセージを送信する:

pingTimer.setInterval(5000);
connect(&pingTimer, &QTimer::timeout, this, &DtlsAssociation::pingTimeout);

startHandshake()は、サーバーとのハンドシェイクを開始する:

void DtlsAssociation::startHandshake()
{
    if (socket.state() != QAbstractSocket::ConnectedState) {
        emit infoMessage(tr("%1: connecting UDP socket first ...").arg(name));
        connect(&socket, &QAbstractSocket::connected, this, &DtlsAssociation::udpSocketConnected);
        return;
    }

    if (!crypto.doHandshake(&socket))
        emit errorMessage(tr("%1: failed to start a handshake - %2").arg(name, crypto.dtlsErrorString()));
    else
        emit infoMessage(tr("%1: starting a handshake").arg(name));
}

readyRead()スロットは、サーバーから送信されたデータグラムを読み込む:

QByteArray dgram(socket.pendingDatagramSize(), Qt::Uninitialized);
const qint64 bytesRead = socket.readDatagram(dgram.data(), dgram.size());
if (bytesRead <= 0) {
    emit warningMessage(tr("%1: spurious read notification?").arg(name));
    return;
}

dgram.resize(bytesRead);

readyRead()スロットは、サーバーから送信されたデータグラムを読み込む。 ハンドシェイクがすでに完了していれば、このデータグラムは復号化される:

if (crypto.isConnectionEncrypted()) {
    const QByteArray plainText = crypto.decryptDatagram(&socket, dgram);
    if (plainText.size()) {
        emit serverResponse(name, dgram, plainText);
        return;
    }

    if (crypto.dtlsError() == QDtlsError::RemoteClosedConnectionError) {
        emit errorMessage(tr("%1: shutdown alert received").arg(name));
        socket.close();
        pingTimer.stop();
        return;
    }

    emit warningMessage(tr("%1: zero-length datagram received?").arg(name));
} else {

そうでなければ、ハンドシェイクを継続する:

    if (!crypto.doHandshake(&socket, dgram)) {
        emit errorMessage(tr("%1: handshake error - %2").arg(name, crypto.dtlsErrorString()));
        return;
    }

ハンドシェイクが完了したら、最初のpingメッセージを送信する:

    if (crypto.isConnectionEncrypted()) {
        emit infoMessage(tr("%1: encrypted connection established!").arg(name));
        pingTimer.start();
        pingTimeout();
    } else {

pskRequired()スロットは、ハンドシェークフェーズで必要な事前共有鍵(PSK)を提供する:

void DtlsAssociation::pskRequired(QSslPreSharedKeyAuthenticator *auth)
{
    Q_ASSERT(auth);

    emit infoMessage(tr("%1: providing pre-shared key ...").arg(name));
    auth->setIdentity(name.toLatin1());
    auth->setPreSharedKey(QByteArrayLiteral("\x1a\x2b\x3c\x4d\x5e\x6f"));
}

注: 簡略化のため、pskRequired()の定義は単純化しすぎている。QSslPreSharedKeyAuthenticator クラスのドキュメントに、このスロットの適切な実装方法が詳しく説明されている。

pingTimeout()は、暗号化されたメッセージをサーバーに送信する:

void DtlsAssociation::pingTimeout()
{
    static const QString message = QStringLiteral("I am %1, please, accept our ping %2");
    const qint64 written = crypto.writeDatagramEncrypted(&socket, message.arg(name).arg(ping).toLatin1());
    if (written <= 0) {
        emit errorMessage(tr("%1: failed to send a ping - %2").arg(name, crypto.dtlsErrorString()));
        pingTimer.stop();
        return;
    }

    ++ping;
}

ハンドシェーク・フェーズの間、クライアントはパケット・ロスのために起こりうるタイムアウトを処理しなければなりません。handshakeTimeout() スロットは、ハンドシェーク・メッセージを再送する:

void DtlsAssociation::handshakeTimeout()
{
    emit warningMessage(tr("%1: handshake timeout, trying to re-transmit").arg(name));
    if (!crypto.handleTimeout(&socket))
        emit errorMessage(tr("%1: failed to re-transmit - %2").arg(name, crypto.dtlsErrorString()));
}

クライアント接続が破棄される前に、そのDTLS接続はシャットダウンされなければならない:

DtlsAssociation::~DtlsAssociation()
{
    if (crypto.isConnectionEncrypted())
        crypto.shutdown(&socket);
}

サーバーからのエラーメッセージ、情報メッセージ、復号化されたレスポンスがUIに表示されます:

const QString colorizer(QStringLiteral("<font color=\"%1\">%2</font><br>"));

void MainWindow::addErrorMessage(const QString &message)
{
    ui->clientMessages->insertHtml(colorizer.arg(QStringLiteral("Crimson"), message));
}

void MainWindow::addWarningMessage(const QString &message)
{
    ui->clientMessages->insertHtml(colorizer.arg(QStringLiteral("DarkOrange"), message));
}

void MainWindow::addInfoMessage(const QString &message)
{
    ui->clientMessages->insertHtml(colorizer.arg(QStringLiteral("DarkBlue"), message));
}

void MainWindow::addServerResponse(const QString &clientInfo, const QByteArray &datagram,
                                   const QByteArray &plainText)
{
    static const QString messageColor = QStringLiteral("DarkMagenta");
    static const QString formatter = QStringLiteral("<br>---------------"
                                                    "<br>%1 received a DTLS datagram:<br> %2"
                                                    "<br>As plain text:<br> %3");

    const QString html = formatter.arg(clientInfo, QString::fromUtf8(datagram.toHex(' ')),
                                       QString::fromUtf8(plainText));
    ui->serverMessages->insertHtml(colorizer.arg(messageColor, html));
}

サンプルプロジェクト @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。