1 证书生成
因为目标是实现双向认证,所以需要将自己的公钥和私钥以及对端的私钥加载到Qt的安全环境中。证书可借助keytool和openssl工具生成,总结几个比较常用的生成命令如下:
# 生成jks格式密钥库
keytool -genkey -v -alias tomcat -keyalg RSA -keystore tomcat.keystore -validity 36500
# 从jks格式密钥库中导出证书(DER格式)
keytool -keystore tomcat.keystore -export -alias tomcat -file server.cer -storepass 123456
# 生成p12格式密钥库
keytool -genkey -v -alias mykey -keyalg RSA -storetype PKCS12 -keystore mykey.p12 -storepass 123456
# 将jks格式密钥库转化为p12格式
keytool -importkeystore -srckeystore tomcat.keystore -destkeystore tomcat.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass 123456 -deststorepass 123456 -srcalias tomcat -destalias tomcat -srckeypass 123456 -destkeypass 123456 -noprompt
# 从p12密钥库中导出公钥和私钥(PEM格式)
openssl pkcs12 -clcerts -nokeys -in mykey.p12 -out cert.pem
openssl pkcs12 -nocerts -nodes -in mykey.p12 -out private.pem
# 向jks格式密钥库中导入可信任的证书
keytool -import -v -file cert.pem -keystore clients.keystore -storepass 123456
2 QSslSocket设置
首先客户端和服务器都必须加载本地的私钥、证书和信任库。在QSslSocket中这三个设置分别对应localCertificate, privateKey和caCertificates。同时双向认证需要设置VerifyPeer和Depth = 1。以客户端为例,加载方法如下:
bool ClientSimulator::loadSslFiles()
{
bool openOk = false;
QFile certFile(QDir::currentPath() + QString("/sslCert/server.cer"));
openOk = certFile.open(QIODevice::ReadOnly);
m_certificate = QSslCertificate(certFile.readAll(), QSsl::Der);
openOk &= !m_certificate.isNull();
QFile keyFile(QDir::currentPath() + QString("/sslCert/ckey.pem"));
openOk &= keyFile.open(QIODevice::ReadOnly);
m_privateKey = QSslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
openOk &= !m_privateKey.isNull();
QFile peerFile(QDir::currentPath() + QString("/sslCert/cert.pem"));
openOk &= peerFile.open(QIODevice::ReadOnly);
QSslCertificate peerCert(peerFile.readAll(), QSsl::Pem);
bool peerCertValid = !peerCert.isNull();
openOk &= peerCertValid;
QList<QSslCertificate> caCerts;
caCerts << peerCert;
m_caCertificates = caCerts;
return openOk;
}
之后在客户端连接到服务器时,设置加载好的证书和密钥:
if(loadSslFiles())
{
m_socket->setLocalCertificate(m_certificate);
m_socket->setPrivateKey(m_privateKey);
m_socket->setCaCertificates(m_caCertificates);
m_socket->setPeerVerifyMode(QSslSocket::VerifyPeer);
m_socket->setPeerVerifyDepth(1);
m_socket->connectToHostEncrypted(ui->lineEditIP->text(), port);
}
else
{
QMessageBox::warning(this, "SSL File Error", "Load SSL Files failed.");
}
这里m_socket是QSslSocket类的实例。
服务器端的设置类似,重载incommingConnection方法,参考实现如下:
void SslServer::incomingConnection(qintptr socketDescriptor)
{
if(!m_client.isNull())
{
m_client->disconnectFromHost();
disconnect(m_client, SIGNAL(readyRead()), this, SLOT(onRecvFromClient()));
disconnect(m_client, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(onSslErrors(QList<QSslError>)));
delete m_client;
}
m_client = new QSslSocket(this);
m_client->setSocketDescriptor(socketDescriptor);
if(m_sslConfig != NULL)
{
m_client->setLocalCertificate(m_sslConfig->certificate());
m_client->setPrivateKey(m_sslConfig->privateKey());
m_client->setCaCertificates(m_sslConfig->caCertificates());
}
m_client->setPeerVerifyMode(QSslSocket::VerifyPeer);
m_client->setPeerVerifyDepth(1);
connect(m_client, SIGNAL(readyRead()), this, SLOT(onRecvFromClient()));
connect(m_client, SIGNAL(sslErrors(const QList<QSslError> &)), this, SLOT(onSslErrors(const QList<QSslError> &)));
m_client->startServerEncryption();
QTcpServer::incomingConnection(socketDescriptor);
}
注意:
- 在客户端连接服务端时,要调用加密连接方法connectToHostEncrypted()。如果使用普通的链接connectToHost()方法,会报无效套接字的错。
- 同理,在服务端连接客户端时,需调用加密通信方法startServerEncryption()。
- 连接的IP要和信任库中证书所提供的IP一致,否则可能会出现IP不匹配的告警。
- 如果ssl环境设置需要在多个地方复用,可以将设置统一加载到QSslConfiguration类的实例中,之后通过QSslSocket的setSslConfiguration方法进行加载。QSslConfiguration提供的接口与上面范例中的比较类似,这里就不赘述了。
- 除上述直接载入证书和秘钥文件的方法外,qt5.4之后还支持直接从pkcs12格式的文件解析并载入证书,调用静态方法QSslCertificate::importPkcs12()即可,范例如下:
QFile keyFile("/certs/ks.p12");
bool openOK = keyFile.open(QIODevice::ReadWrite);
QSslKey key;
QSslCertificate certs;
QList<QSslCertificate> caCerts;
QByteArray passPhrase = QString("test123").toLatin1();
openOK = QSslCertificate::importPkcs12(&keyFile, &key, &certs, &caCerts, passPhrase);
keyFile.close();