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> &params)
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"