WebEngine ウィジェット クライアント証明書の例

Qt WebEngine とQSslServer を使用したシンプルなクライアント証明書認証シナリオです。

この例では、クライアント証明書認証のワークフローを示します。この認証シナリオは、例えば組み込みデバイスに実装することができ、組み込みデバイスはその機能を処理するためのウェブインターフェースを提供します。管理者は Qt WebEngine クライアントを使用して組み込みデバイスを管理し、カスタム SSL 証明書を使用して認証を行います。接続はSSLソケットで暗号化されます。組み込みデバイスはQSslSocket を使用して認証と暗号化を処理します。この方法では、管理者は認証情報を入力する必要はなく、デバイスが認識する適切な証明書を選択するだけでよい。

この例では、ワークフローを示すために、非常にシンプルで最小限のアプローチに焦点を当てている。QSslSocket は、リソースの限られた組み込みデバイス上で本格的な HTTPS サーバを実行する必要がないため、低レベルのソリューションであることに注意してください。

証明書の作成

この例ではすでに証明書が生成されていますが、新しい証明書を生成する方法を見てみましょう。OpenSSLツールを使ってサーバとクライアントの証明書を作成します。

まず、証明書署名要求CSR を作成し、署名します。CAの秘密鍵を使用して、クライアントとサーバーの両方のローカル証明書に署名し、発行します。

openssl req -out ca.pem -new -x509 -nodes -keyout ca.key

注: -days オプションを指定して、デフォルトの証明書有効期間である30日をオーバーライドします。

では、クライアントとサーバー用に2つの秘密鍵を作成しましょう:

openssl genrsa -out client.key 2048
openssl genrsa -out server.key 2048

次に、2つの証明書署名要求が必要です:

openssl req -key client.key -new -out client.req
openssl req -key server.key -new -out server.req

両方の証明書をCSRから発行しよう:

openssl x509 -req -in client.req -out client.pem -CA ca.pem -CAkey ca.key
openssl x509 -req -in server.req -out server.pem -CA ca.pem -CAkey ca.key

クライアント証明書のサブジェクトとシリアル番号は、認証時に選択できるように表示される。シリアル番号は印刷することができる:

openssl x509 -serial -noout -in client.pem

クライアントの実装

これでウェブ・ブラウザ・クライアントを実装できる。

まず、証明書とその秘密鍵をロードし、QSslCertificateQSslKey インスタンスを作成します。

    QFile certFile(":/resources/client.pem");
    certFile.open(QIODevice::ReadOnly);
    const QSslCertificate cert(certFile.readAll(), QSsl::Pem);

    QFile keyFile(":/resources/client.key");
    keyFile.open(QIODevice::ReadOnly);
    const QSslKey sslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, "");

次に、証明書とその秘密鍵をQWebEngineClientCertificateStore に追加します。

    QWebEngineProfile::defaultProfile()->clientCertificateStore()->add(cert, sslKey);

証明書を扱うには、QWebEnginePage のインスタンスを作成し、QWebEnginePage::certificateErrorQWebEnginePage::selectClientCertificate の2つのシングルに接続する必要がある。自己署名サーバー証明書は証明書エラーを引き起こすため、最初の証明書だけが必要である。本番環境では、自己署名証明書は使用されない。したがって、この例では、適切な証明書の提供を避けるために、QWebEngineCertificateError 。秘密鍵は秘密であり、決して公開すべきではないことに注意。

    QWebEnginePage page;
    QObject::connect(&page, &QWebEnginePage::certificateError,
                     [](QWebEngineCertificateError e) { e.acceptCertificate(); });

QWebEnginePage::selectClientCertificate 、単にQDialog を表示し、QListWidget はクライアント証明書のリストを表示する。ユーザーが選択した証明書は、QWebEngineClientCertificateSelection::select の呼び出しに渡される。

    QObject::connect(
            &page, &QWebEnginePage::selectClientCertificate, &page,
            [&cert](QWebEngineClientCertificateSelection selection) {
                QDialog dialog;
                QVBoxLayout *layout = new QVBoxLayout;
                QLabel *label = new QLabel(QLatin1String("Select certificate"));
                QListWidget *listWidget = new QListWidget;
                listWidget->setSelectionMode(QAbstractItemView::SingleSelection);
                QPushButton *button = new QPushButton(QLatin1String("Select"));
                layout->addWidget(label);
                layout->addWidget(listWidget);
                layout->addWidget(button);
                QObject::connect(button, &QPushButton::clicked, [&dialog]() { dialog.accept(); });
                const QList<QSslCertificate> &list = selection.certificates();
                for (const QSslCertificate &cert : list) {
                    listWidget->addItem(cert.subjectDisplayName() + " : " + cert.serialNumber());
                }
                dialog.setLayout(layout);
                if (dialog.exec() == QDialog::Accepted)
                    selection.select(list[listWidget->currentRow()]);
                else
                    selection.selectNone();
            });

最後に、QWebEnginePage 用にQWebEngineView を作成し、サーバーの URL をロードして、ページを表示する。

    QWebEngineView view(&page);
    view.setUrl(QUrl("https://localhost:5555"));
    view.resize(800, 600);
    view.show();

サーバーの実装

組み込みデバイスのために、最小限の HTTPS サーバを開発します。着信接続を処理し、QSslSocket インスタンスを提供するために、QSslServer を使用することができます。そのために、QSslServer のインスタンスを作成し、クライアントのセットアップと同様に、サーバー証明書とその秘密鍵をロードする。次に、QSslCertificateQSslKey オブジェクトを作成する。さらに、サーバーがクライアントから提示された証明書を検証できるように、CA証明書が必要です。CA証明書とローカル証明書はQSslConfiguration にセットされ、後でサーバーによって使用される。

    QSslServer server;
    QSslConfiguration configuration(QSslConfiguration::defaultConfiguration());
    configuration.setPeerVerifyMode(QSslSocket::VerifyPeer);

    QFile keyFile(":/resources/server.key");
    keyFile.open(QIODevice::ReadOnly);

    QSslKey key(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
    configuration.setPrivateKey(key);

    QList<QSslCertificate> localCerts = QSslCertificate::fromPath(":/resources/server.pem");
    configuration.setLocalCertificateChain(localCerts);

    QList<QSslCertificate> caCerts = QSslCertificate::fromPath(":resources/ca.pem");
    configuration.addCaCertificates(caCerts);

    server.setSslConfiguration(configuration);

次に、サーバーがポート5555

    if (!server.listen(QHostAddress::LocalHost, 5555))
        qFatal("Could not start server on localhost:5555");
    else
        qInfo("Server started on localhost:5555");

QTcpServer::pendingConnectionAvailable シグナルにラムダ関数を用意し、着信接続の処理を実装します。このシグナルは、認証が成功し、socket TLS暗号化が開始された後にトリガーされる。

    QObject::connect(&server, &QTcpServer::pendingConnectionAvailable, [&server]() {
        QTcpSocket *socket = server.nextPendingConnection();
        Q_ASSERT(socket);

        QPointer<Request> request(new Request);

        QObject::connect(socket, &QAbstractSocket::disconnected, socket,
                         [socket, request]() mutable {
                             delete request;
                             socket->deleteLater();
                         });

Request QPointer オブジェクトは、 の単純なラッパーである。このオブジェクトは受信HTTPデータを収集する。リクエストが完了するか、ソケットが終了すると削除される。QByteArray

struct Request : public QObject
{
    QByteArray m_data;
};

リクエストに対する応答はリクエストされたURLに依存し、HTMLページの形でソケットを通して送り返される。GET rootリクエストに対して、管理者はAccess Granted メッセージとExit HTMLボタンを見る。管理者がそれをクリックすると、クライアントは別の リクエストを送る。今度は/exit 相対URLで、これがサーバー終了のトリガーとなる。

        QObject::connect(socket, &QTcpSocket::readyRead, socket, [socket, request]() mutable {
            request->m_data.append(socket->readAll());

            if (!request->m_data.endsWith("\r\n\r\n"))
                return;

            socket->write(http_ok);
            socket->write(html_start);

            if (request->m_data.startsWith("GET / ")) {
                socket->write("<p>ACCESS GRANTED !</p>");
                socket->write("<p>You reached the place, where no one has gone before.</p>");
                socket->write("<button onclick=\"window.location.href='/exit'\">Exit</button>");
            } else if (request->m_data.startsWith("GET /exit ")) {
                socket->write("<p>BYE !</p>");
                socket->write("<p>Have good day ...</p>");
                QTimer::singleShot(0, &QCoreApplication::quit);
            } else {
                socket->write("<p>There is nothing to see here.</p>");
            }

            socket->write(html_end);
            delete request;
            socket->disconnectFromHost();
        });

server client証明書を選択すると、Access Granted ページが表示される。

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

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