Qtの基礎 - パケット

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要



AESを使用した暗号化

以下の例では、QAESEncryptionクラスを使用して、AES(CBCモード)暗号化を指定して、パケットデータを暗号化している。
また、パケット長を計算して、ネットワークバイトオーダーに変換している。
最後に、QTcpSocket::writeメソッドを使用して、パケットを送信している。

 #include <QtNetwork>
 #include <QDataStream>
 #include <QCryptographicHash>
 #include <QIODevice>
 
 // パケットデータをAES暗号化
 QByteArray encryptPacket(const QByteArray &data, const QByteArray &key, const QByteArray &iv)
 {
    // AES(CBCモード)の指定
    QAESEncryption encryption(QAESEncryption::AES_128, QAESEncryption::CBC);
    encryption.setKey(key);
    encryption.setIv(iv);
 
    // データを暗号化
    QByteArray encrypted = encryption.encode(data);
    return encrypted;
 }
 
 // パケットの送信
 void sendPacket(QTcpSocket *socket, const QByteArray &data, const QByteArray &key, const QByteArray &iv)
 {
    // パケットデータをAES暗号化
    QByteArray encryptedData = encryptPacket(data, key, iv);
 
    // パケット長の計算
    quint16 packetLength = encryptedData.size();
 
    // パケット長をネットワークバイトオーダーへ変換
    QByteArray packetLengthBytes;
    QDataStream lengthStream(&packetLengthBytes, QIODevice::WriteOnly);
    lengthStream.setByteOrder(QDataStream::BigEndian);
    lengthStream << packetLength;
 
    // パケットの送信
    socket->write(packetLengthBytes);
    socket->write(encryptedData);
 }
 
 int main()
 {
    QTcpSocket socket;
 
    // 接続処理
    QByteArray key = "your_key";          // 鍵
    QByteArray iv  = "your_iv";           // 初期化ベクトル
    QByteArray packetData = "your_data";  // 送信するパケット
 
    sendPacket(&socket, packetData, key, iv);
 }


以下の例では、QAESEncryptionクラスを使用して、AES(CBCモード)復号を指定して、パケットを復号している。
まず、パケット長を受信して、次に、パケットを受信してAES復号する。

 #include <QtNetwork>
 #include <QDataStream>
 #include <QCryptographicHash>
 #include <QIODevice>
 
 // パケットをAES復号
 QByteArray decryptPacket(const QByteArray &data, const QByteArray &key, const QByteArray &iv)
 {
    // AES(CBCモード)復号を指定
    QAESEncryption decryption(QAESEncryption::AES_128, QAESEncryption::CBC);
    decryption.setKey(key);
    decryption.setIv(iv);
 
    // データを復号
    QByteArray decrypted = decryption.decode(data);
 
    return decrypted;
 }
 
 // パケットの受信
 QByteArray receivePacket(QTcpSocket *socket, const QByteArray &key, const QByteArray &iv)
 {
    QByteArray packetData;
    qint64 packetLength = -1;
 
    // パケット長の受信
    while(socket->bytesAvailable() < (qint64)sizeof(packetLength))
    {
       if(!socket->waitForReadyRead())
       {
          return QByteArray();
       }
    }
    QDataStream lengthStream(socket);
    lengthStream >> packetLength;
 
    // パケットの受信
    while (socket->bytesAvailable() < packetLength)
    {
       if(!socket->waitForReadyRead())
       {
          return QByteArray();
       }
    }
    packetData = socket->read(packetLength);
 
    // パケットのAES復号
    QByteArray decryptedData = decryptPacket(packetData, key, iv);
 
    return decryptedData;
 }
 
 int main()
 {
    QTcpSocket socket;
 
    // 接続処理
    QByteArray key = "your_key";  // 鍵
    QByteArray iv  = "your_iv";   // 初期化ベクトル
 
    QByteArray receivedPacket = receivePacket(&socket, key, iv);
 }



SSLを使用した暗号化

暗号化

QSslSocketクラスは、暗号化されたデータの送信に使用できる安全で暗号化されたTCP接続を確立する。
クライアントモードとサーバーモードの両方で動作し、TLS 1.3を含む最新のTLSプロトコルをサポートしている。

デフォルトでは、QSslSocketクラスは、安全であると考えられているTLSプロトコル(QSsl::SecureProtocols)のみを使用するが、
ハンドシェイクが始まる前に行う限り、setProtocolメソッドを呼び出してTLSプロトコルを変更することが可能である。

SSL暗号化は、ソケットがConnectedStateになった後、既存のTCPストリームの上で動作する。

QSslSocketクラスを使用して安全な接続を確立するには、以下に示す2つの方法がある。

  • 即座にSSLハンドシェイクを行う方法
  • 非暗号化モードで接続が確立された後、遅延SSLハンドシェイクを行う方法


QSslSocketクラスの最も一般的な使用方法は、QSslSocketクラスのインスタンスを生成して、connectToHostEncryptedメソッドを呼び出して、安全な接続を開始する方法である。
connectToHostEncryptedメソッドは、接続が確立されると即座にSSLハンドシェイクを開始する。

 auto pSocket = std::make_unique<QSslSocket>(this);
 connect(pSocket, &QSslSocket::encrypted, this, &CSampleClass::ready);
 
 pSocket->connectToHostEncrypted("example.com", 993);


プレーンなQTcpSocketクラスと同様、QSslSocketクラスはHostLookupState -> ConnectingStateになり、接続が成功するとConnectedStateになる。
その後、自動的にハンドシェイクが開始されて、成功するとソケットが暗号化状態になり、使用可能な状態になったことを示すencryptedシグナルが送信される。
この時、connectToHostEncryptedメソッドのリターン直後(encryptedシグナルが送信される前)に、ソケットにデータを書き込むことができることに注意する。

データは、encryptedシグナルが送信されるまで、QSslSocketクラスにキューイングされる。<br<
既存の接続を保護するために遅延SSLハンドシェイクを使用する例として、SSLサーバが着信接続を保護する場合がある。

以下の例のように、QTcpServerクラスを継承して、SslServerクラスを定義したとする。
まず、QSslSocketクラスのインスタンスを生成した後、setSocketDescriptorメソッドを呼んで、新しいソケットのディスクリプタを渡された既存のものに設定する。
そして、QTcpServer::incomingConnectionメソッドをオーバーライドして、そのメソッド内でstartServerEncryptionメソッドを呼び出すことにより、SSLハンドシェイクを開始する。

 void SslServer::incomingConnection(qintptr socketDescriptor)
 {
    auto pServerSocket = std::make_unique<QSslSocket>();
    if(pServerSocket->setSocketDescriptor(socketDescriptor))
    {
       addPendingConnection(pServerSocket);
       connect(pServerSocket, &QSslSocket::encrypted, this, &SslServer::ready);
       pServerSocket->startServerEncryption();
    }
 }


エラーが発生した場合、QSslSocketクラスはsslErrorsシグナルを送信する。
この時、エラーを無視するためのアクションが取られない場合は、接続は切断される。

エラーが発生しても接続を継続する場合は、エラー発生後にこのスロット内、または、QSslSocketクラスのインスタンス生成後、接続が試行される前であればignoreSslErrorsメソッドを呼び出すことができる。
これは、QSslSocketクラスが相手のIDを確立する時に遭遇したエラーを無視することができる。

安全な接続は成功したハンドシェイクで確立されるべきであるため、SSLハンドシェイク中のエラーを無視することは慎重になるべきである。

1度暗号化されると、QSslSocketクラスは通常のQTcpSocketクラスとして使用される。
readyReadシグナルが送信される時、readメソッド、canReadLineメソッド、readLineメソッド、getCharメソッドを使用してQSslSocketクラスの内部バッファから復号したデータを読み、
writeメソッド、putCharメソッドを使用して相手にデータを書き戻すことができる。

QSslSocketクラスは、書き込まれたデータを自動的に暗号化して、データがピアに書き込まれるとencryptedBytesWrittenシグナルを送信する。

便利なことに、QSslSocketクラスは、QTcpSocketクラスのブロッキングメソッドである以下に示すメソッドが存在する。

  • waitForConnectedメソッド
  • waitForReadyReadメソッド
  • waitForBytesWrittenメソッド
  • waitForDisconnectedメソッド


また、暗号化された接続が確立されるまで呼び出し側のスレッドをブロックするwaitForEncryptedメソッドも提供されている。

 QSslSocket socket;
 socket.connectToHostEncrypted("http.example.com", 443);
 if(!socket.waitForEncrypted())
 {
    qDebug() << socket.errorString();
    return false;
 }
 
 socket.write("GET / HTTP/1.0\r\n\r\n");
 while(socket.waitForReadyRead())
 {
    qDebug() << socket.readAll().data();
 }


QSslSocketクラスは、暗号、秘密鍵、ローカル証明書、ピア証明書、認証局(CA)証明書を扱うための広範で使い勝手の良いAPIを提供する。
また、ハンドシェイクフェーズで発生したエラーを処理するためのAPIも提供している。

また、以下の機能をカスタマイズすることも可能である。

  • QSslConfiguration::setCiphersメソッド、および、QSslConfiguration::setDefaultCiphersメソッドにより、ハンドシェイク前にソケットの暗号化スイートをカスタマイズすることができる。
  • ソケットのローカル証明書と秘密鍵は、ハンドシェイクフェーズの前にsetLocalCertificateメソッドとsetPrivateKeyメソッドを使用して、カスタマイズすることができる。
  • QSslConfiguration::addCaCertificateメソッド、QSslConfiguration::addCaCertificatesメソッドを使用することにより、CA証明書データベースを拡張し、カスタマイズすることができる。


SSLハンドシェイク中にSSLソケットで使用されるデフォルトCA証明書のリストを拡張するには、デフォルトの設定を更新する必要がある。

 QList<QSslCertificate> certificates = getCertificates();
 
 QSslConfiguration configuration = QSslConfiguration::defaultConfiguration();
 configuration.addCaCertificates(certificates);
 
 QSslConfiguration::setDefaultConfiguration(configuration);


自己署名証明書を使用する場合

自己署名証明書と秘密鍵を作成する。

openssl req -x509 -sha256 -newkey rsa:3072 \
        -subj "/C=JP/ST=Tokyo/L=Tokyo City/O=Company Name/OU=Department/CN=localhost" \
        -keyout sshconfig.key -out sshconfig.pem -days 3650 -nodes


自己署名証明書をインストールする場合

自己署名証明書を信頼済みのルート証明書としてインストールする。

まず、自己署名証明書と秘密鍵を作成する。

openssl req -x509 -sha256 -newkey rsa:3072 \
        -subj "/C=JP/ST=Tokyo/L=Tokyo City/O=Company Name/OU=Department/CN=localhost" \
        -keyout sshconfig.key -out sshconfig.pem -days 3650 -nodes


次に、自己署名証明書を、以下に示すディレクトリに配置する。

  • /etc/pki/trust/anchors
    当ディレクトリに配置する場合は、サブディレクトリを作成してもよい。
  • /usr/share/pki/trust/anchors
    当ディレクトリに配置する場合は、サブディレクトリは作成しない。
sudo cp sshconfig.pem /etc/pki/trust/anchors
# または
sudo cp sshconfig.pem /usr/share/pki/trust/anchors


最後に、証明書を信頼済みルート証明書に追加する。

sudo update-ca-certificates


自己署名証明書をアンインストールする。

sudo rm /etc/pki/trust/anchors/sshconfig.pem
sudo update-ca-certificates


サンプルコード

以下の例では、OpenSSLライブラリとQTcpSocketクラスを使用して、パケットを暗号化して送信している。

 #include <QTcpSocket>
 #include <QSslSocket>
 #include <QSslKey>
 #include <QSslCertificate>
 #include <QFile>
 
 // 暗号化
 void encryptPacket(QByteArray &packet)
 {
    // OpenSSLライブラリの初期化
    SSL_load_error_strings();
    SSL_library_init();
 
    // 公開鍵と秘密鍵を読み込む
    QSslKey privateKey("server.key", QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
    QSslCertificate publicKey("server.crt", QSsl::Pem);
 
    // QSslSocketオブジェクトの生成
    QSslSocket socket;
    socket.setPrivateKey(privateKey);
    socket.setLocalCertificate(publicKey);
    socket.connectToHostEncrypted("example.com", 443);
 
    // パケットの暗号化
    packet = socket.encrypt(packet);
 }
 
 int main()
 {
    // パケットの生成
    QByteArray packet = "Hello, world!";
 
    // パケットの暗号化
    encryptPacket(packet);
 
    // 暗号化されたパケットの送信
    QTcpSocket socket;
    socket.connectToHost("example.com", 1234);  // ホスト名とTCPポート番号の指定
    socket.write(packet);
 
    // 送信が完了するまで待機
    socket.waitForBytesWritten();
 
    return 0;
 }


以下の例では、暗号化されたパケットを送信するために、TCPソケットとパケットデータを引数として受け取り、OpenSSLライブラリを使用してSSLセッションを確立して、パケットを暗号化して送信する。
SSLセッションは、QSslSocketクラスを使用して確立される。

この時、事前に、サーバの証明書、クライアントの秘密鍵、サーバの公開鍵をファイルから読み込む必要がある。

 #include <QTcpSocket>
 #include <QSslSocket>
 #include <QSslKey>
 #include <QSslCertificate>
 #include <QFile>
 
 void sendEncryptedPacket(QTcpSocket *socket, const QByteArray &packet)
 {
    // OpenSSLライブラリの初期化
    SSL_load_error_strings();
    SSL_library_init();
 
    // SSLコンテキストを生成
    SSL_CTX *ctx = SSL_CTX_new(TLSv1_2_client_method());
 
    // サーバの証明書を読み込む
    QFile certFile("server.crt");
    certFile.open(QIODevice::ReadOnly);
    QSslCertificate cert(certFile.readAll());
    certFile.close();
    SSL_CTX_use_certificate(ctx, cert.handle());
 
    // クライアントの秘密鍵を読み込む
    QFile keyFile("client.key");
    keyFile.open(QIODevice::ReadOnly);
    QSslKey key(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, "my passphrase");
    keyFile.close();
    SSL_CTX_use_PrivateKey(ctx, key.handle());
 
    // サーバの公開鍵を読み込む
    QFile pubkeyFile("server.pub");
    pubkeyFile.open(QIODevice::ReadOnly);
    QSslCertificate pubkey(pubkeyFile.readAll());
    pubkeyFile.close();
    SSL_CTX_add_extra_chain_cert(ctx, pubkey.handle());
 
    // SSLセッションの確立
    QSslSocket sslSocket;
    sslSocket.setSocketDescriptor(socket->socketDescriptor());
    sslSocket.setProtocol(QSsl::TlsV1_2);
    sslSocket.setLocalCertificate(cert);
    sslSocket.setPrivateKey(key);
    sslSocket.addCaCertificate(pubkey);
    sslSocket.startClientEncryption();
 
    // パケットの暗号化
    QByteArray encryptedPacket = sslSocket.encrypt(packet);
 
    // 暗号化されたパケットの送信
    socket->write(encryptedPacket);
    socket->flush();
    socket->waitForBytesWritten();
 
    // SSLセッションの終了
    sslSocket.close();
 }