File indexing completed on 2024-12-22 04:57:00
0001 /* 0002 SPDX-FileCopyrightText: 2018 Krzysztof Nowicki <krissn@op.pl> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "ewspkeyauthjob.h" 0008 0009 #include <QNetworkAccessManager> 0010 #include <QNetworkReply> 0011 #include <QNetworkRequest> 0012 #include <QUrlQuery> 0013 0014 #include <QtCrypto> 0015 0016 static const QMap<QString, QCA::CertificateInfoTypeKnown> stringToKnownCertInfoType = { 0017 {QStringLiteral("CN"), QCA::CommonName}, 0018 {QStringLiteral("L"), QCA::Locality}, 0019 {QStringLiteral("ST"), QCA::State}, 0020 {QStringLiteral("O"), QCA::Organization}, 0021 {QStringLiteral("OU"), QCA::OrganizationalUnit}, 0022 {QStringLiteral("C"), QCA::Country}, 0023 {QStringLiteral("emailAddress"), QCA::EmailLegacy}, 0024 }; 0025 0026 static QMultiMap<QCA::CertificateInfoType, QString> parseCertSubjectInfo(const QString &info) 0027 { 0028 QMultiMap<QCA::CertificateInfoType, QString> map; 0029 const auto infos{info.split(QLatin1Char(','), Qt::SkipEmptyParts)}; 0030 for (const auto &token : infos) { 0031 const auto keyval = token.trimmed().split(QLatin1Char('=')); 0032 if (keyval.count() == 2) { 0033 if (stringToKnownCertInfoType.contains(keyval[0])) { 0034 map.insert(stringToKnownCertInfoType[keyval[0]], keyval[1]); 0035 } 0036 } 0037 } 0038 0039 return map; 0040 } 0041 0042 static QString escapeSlashes(const QString &str) 0043 { 0044 QString result = str; 0045 return result.replace(QLatin1Char('/'), QStringLiteral("\\/")); 0046 } 0047 0048 EwsPKeyAuthJob::EwsPKeyAuthJob(const QUrl &pkeyUri, const QString &certFile, const QString &keyFile, const QString &keyPassword, QObject *parent) 0049 : EwsJob(parent) 0050 , mPKeyUri(pkeyUri) 0051 , mCertFile(certFile) 0052 , mKeyFile(keyFile) 0053 , mKeyPassword(keyPassword) 0054 , mNetworkAccessManager(new QNetworkAccessManager(this)) 0055 { 0056 } 0057 0058 EwsPKeyAuthJob::~EwsPKeyAuthJob() 0059 { 0060 } 0061 0062 void EwsPKeyAuthJob::start() 0063 { 0064 const QUrlQuery query(mPKeyUri); 0065 QMap<QString, QString> params; 0066 for (const auto &it : query.queryItems()) { 0067 params[it.first.toLower()] = QUrl::fromPercentEncoding(it.second.toLatin1()); 0068 } 0069 0070 if (params.contains(QStringLiteral("submiturl")) && params.contains(QStringLiteral("nonce")) && params.contains(QStringLiteral("certauthorities")) 0071 && params.contains(QStringLiteral("context")) && params.contains(QStringLiteral("version"))) { 0072 const auto respToken = buildAuthResponse(params); 0073 0074 if (!respToken.isEmpty()) { 0075 sendAuthRequest(respToken, QUrl(params[QStringLiteral("submiturl")]), params[QStringLiteral("context")]); 0076 } else { 0077 emitResult(); 0078 } 0079 } else { 0080 setErrorMsg(QStringLiteral("Missing one or more input parameters")); 0081 emitResult(); 0082 } 0083 } 0084 0085 void EwsPKeyAuthJob::sendAuthRequest(const QByteArray &respToken, const QUrl &submitUrl, const QString &context) 0086 { 0087 QNetworkRequest req(submitUrl); 0088 0089 req.setRawHeader("Authorization", 0090 QStringLiteral("PKeyAuth AuthToken=\"%1\",Context=\"%2\",Version=\"1.0\"").arg(QString::fromLatin1(respToken), context).toLatin1()); 0091 0092 mAuthReply.reset(mNetworkAccessManager->get(req)); 0093 0094 connect(mAuthReply.data(), &QNetworkReply::finished, this, &EwsPKeyAuthJob::authRequestFinished); 0095 } 0096 0097 void EwsPKeyAuthJob::authRequestFinished() 0098 { 0099 if (mAuthReply->error() == QNetworkReply::NoError) { 0100 mResultUri = mAuthReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); 0101 if (!mResultUri.isValid()) { 0102 setErrorMsg(QStringLiteral("Incorrect or missing redirect URI in PKeyAuth response")); 0103 } 0104 } else { 0105 setErrorMsg(QStringLiteral("Failed to process PKeyAuth request: %1").arg(mAuthReply->errorString())); 0106 } 0107 emitResult(); 0108 } 0109 0110 QByteArray EwsPKeyAuthJob::buildAuthResponse(const QMap<QString, QString> ¶ms) 0111 { 0112 QCA::Initializer init; 0113 0114 if (!QCA::isSupported("cert")) { 0115 setErrorMsg(QStringLiteral("QCA was not built with PKI certificate support")); 0116 return QByteArray(); 0117 } 0118 0119 if (params[QStringLiteral("version")] != QLatin1StringView("1.0")) { 0120 setErrorMsg(QStringLiteral("Unknown version of PKey Authentication: %1").arg(params[QStringLiteral("version")])); 0121 return QByteArray(); 0122 } 0123 0124 const auto authoritiesInfo = parseCertSubjectInfo(params[QStringLiteral("certauthorities")]); 0125 0126 QCA::ConvertResult importResult; 0127 const QCA::CertificateCollection certs = QCA::CertificateCollection::fromFlatTextFile(mCertFile, &importResult); 0128 0129 if (importResult != QCA::ConvertGood) { 0130 setErrorMsg(QStringLiteral("Certificate import failed")); 0131 return QByteArray(); 0132 } 0133 0134 QCA::Certificate cert; 0135 const auto certificates = certs.certificates(); 0136 for (const auto &c : certificates) { 0137 if (c.issuerInfo() == authoritiesInfo) { 0138 cert = c; 0139 break; 0140 } 0141 } 0142 0143 if (cert.isNull()) { 0144 setErrorMsg(QStringLiteral("No suitable certificate found")); 0145 return QByteArray(); 0146 } 0147 0148 QCA::PrivateKey privateKey = QCA::PrivateKey::fromPEMFile(mKeyFile, mKeyPassword.toUtf8(), &importResult); 0149 if (importResult != QCA::ConvertGood) { 0150 setErrorMsg(QStringLiteral("Private key import failed")); 0151 return QByteArray(); 0152 } 0153 0154 const QString certStr = escapeSlashes(QString::fromLatin1(cert.toDER().toBase64())); 0155 const QString header = QStringLiteral("{\"x5c\":[\"%1\"],\"typ\":\"JWT\",\"alg\":\"RS256\"}").arg(certStr); 0156 0157 const QString payload = QStringLiteral("{\"nonce\":\"%1\",\"iat\":\"%2\",\"aud\":\"%3\"}") 0158 .arg(params[QStringLiteral("nonce")]) 0159 .arg(QDateTime::currentSecsSinceEpoch()) 0160 .arg(escapeSlashes(params[QStringLiteral("submiturl")])); 0161 0162 const auto headerB64 = header.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); 0163 const auto payloadB64 = payload.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); 0164 0165 QCA::SecureArray data(headerB64 + '.' + payloadB64); 0166 0167 QByteArray sig = privateKey.signMessage(data, QCA::EMSA3_SHA256, QCA::IEEE_1363); 0168 0169 return headerB64 + '.' + payloadB64 + '.' + sig.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); 0170 } 0171 0172 const QUrl &EwsPKeyAuthJob::resultUri() const 0173 { 0174 return mResultUri; 0175 } 0176 0177 QString EwsPKeyAuthJob::getAuthHeader() 0178 { 0179 const QUrlQuery query(mPKeyUri); 0180 QMap<QString, QString> params; 0181 for (const auto &it : query.queryItems()) { 0182 params[it.first.toLower()] = QUrl::fromPercentEncoding(it.second.toLatin1()); 0183 } 0184 0185 if (params.contains(QStringLiteral("submiturl")) && params.contains(QStringLiteral("nonce")) && params.contains(QStringLiteral("certauthorities")) 0186 && params.contains(QStringLiteral("context")) && params.contains(QStringLiteral("version"))) { 0187 const auto respToken = buildAuthResponse(params); 0188 0189 if (!respToken.isEmpty()) { 0190 return QLatin1StringView("PKeyAuth AuthToken=\"%1\",Context=\"%2\",Version=\"1.0\"") 0191 .arg(QString::fromLatin1(respToken), params[QStringLiteral("context")]); 0192 } else { 0193 return {}; 0194 } 0195 } else { 0196 return {}; 0197 } 0198 } 0199 0200 #include "moc_ewspkeyauthjob.cpp"