1 /*
2     SPDX-FileCopyrightText: 2018 Krzysztof Nowicki <krissn@op.pl>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "ewspkeyauthjob.h"
8 
9 #include <QNetworkAccessManager>
10 #include <QNetworkReply>
11 #include <QNetworkRequest>
12 #include <QUrlQuery>
13 
14 #include <QtCrypto>
15 
16 static const QMap<QString, QCA::CertificateInfoTypeKnown> stringToKnownCertInfoType = {
17     {QStringLiteral("CN"), QCA::CommonName},
18     {QStringLiteral("L"), QCA::Locality},
19     {QStringLiteral("ST"), QCA::State},
20     {QStringLiteral("O"), QCA::Organization},
21     {QStringLiteral("OU"), QCA::OrganizationalUnit},
22     {QStringLiteral("C"), QCA::Country},
23     {QStringLiteral("emailAddress"), QCA::EmailLegacy},
24 };
25 
parseCertSubjectInfo(const QString & info)26 static QMultiMap<QCA::CertificateInfoType, QString> parseCertSubjectInfo(const QString &info)
27 {
28     QMultiMap<QCA::CertificateInfoType, QString> map;
29     const auto infos{info.split(QLatin1Char(','), Qt::SkipEmptyParts)};
30     for (const auto &token : infos) {
31         const auto keyval = token.trimmed().split(QLatin1Char('='));
32         if (keyval.count() == 2) {
33             if (stringToKnownCertInfoType.contains(keyval[0])) {
34                 map.insert(stringToKnownCertInfoType[keyval[0]], keyval[1]);
35             }
36         }
37     }
38 
39     return map;
40 }
41 
escapeSlashes(const QString & str)42 static QString escapeSlashes(const QString &str)
43 {
44     QString result = str;
45     return result.replace(QLatin1Char('/'), QStringLiteral("\\/"));
46 }
47 
EwsPKeyAuthJob(const QUrl & pkeyUri,const QString & certFile,const QString & keyFile,const QString & keyPassword,QObject * parent)48 EwsPKeyAuthJob::EwsPKeyAuthJob(const QUrl &pkeyUri, const QString &certFile, const QString &keyFile, const QString &keyPassword, QObject *parent)
49     : EwsJob(parent)
50     , mPKeyUri(pkeyUri)
51     , mCertFile(certFile)
52     , mKeyFile(keyFile)
53     , mKeyPassword(keyPassword)
54     , mNetworkAccessManager(new QNetworkAccessManager(this))
55 {
56 }
57 
~EwsPKeyAuthJob()58 EwsPKeyAuthJob::~EwsPKeyAuthJob()
59 {
60 }
61 
start()62 void EwsPKeyAuthJob::start()
63 {
64     const QUrlQuery query(mPKeyUri);
65     QMap<QString, QString> params;
66     for (const auto &it : query.queryItems()) {
67         params[it.first.toLower()] = QUrl::fromPercentEncoding(it.second.toLatin1());
68     }
69 
70     if (params.contains(QStringLiteral("submiturl")) && params.contains(QStringLiteral("nonce")) && params.contains(QStringLiteral("certauthorities"))
71         && params.contains(QStringLiteral("context")) && params.contains(QStringLiteral("version"))) {
72         const auto respToken = buildAuthResponse(params);
73 
74         if (!respToken.isEmpty()) {
75             sendAuthRequest(respToken, QUrl(params[QStringLiteral("submiturl")]), params[QStringLiteral("context")]);
76         } else {
77             emitResult();
78         }
79     } else {
80         setErrorMsg(QStringLiteral("Missing one or more input parameters"));
81         emitResult();
82     }
83 }
84 
sendAuthRequest(const QByteArray & respToken,const QUrl & submitUrl,const QString & context)85 void EwsPKeyAuthJob::sendAuthRequest(const QByteArray &respToken, const QUrl &submitUrl, const QString &context)
86 {
87     QNetworkRequest req(submitUrl);
88 
89     req.setRawHeader("Authorization",
90                      QStringLiteral("PKeyAuth AuthToken=\"%1\",Context=\"%2\",Version=\"1.0\"").arg(QString::fromLatin1(respToken), context).toLatin1());
91 
92     mAuthReply.reset(mNetworkAccessManager->get(req));
93 
94     connect(mAuthReply.data(), &QNetworkReply::finished, this, &EwsPKeyAuthJob::authRequestFinished);
95 }
96 
authRequestFinished()97 void EwsPKeyAuthJob::authRequestFinished()
98 {
99     if (mAuthReply->error() == QNetworkReply::NoError) {
100         mResultUri = mAuthReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
101         if (!mResultUri.isValid()) {
102             setErrorMsg(QStringLiteral("Incorrect or missing redirect URI in PKeyAuth response"));
103         }
104     } else {
105         setErrorMsg(QStringLiteral("Failed to process PKeyAuth request: %1").arg(mAuthReply->errorString()));
106     }
107     emitResult();
108 }
109 
buildAuthResponse(const QMap<QString,QString> & params)110 QByteArray EwsPKeyAuthJob::buildAuthResponse(const QMap<QString, QString> &params)
111 {
112     QCA::Initializer init;
113 
114     if (!QCA::isSupported("cert")) {
115         setErrorMsg(QStringLiteral("QCA was not built with PKI certificate support"));
116         return QByteArray();
117     }
118 
119     if (params[QStringLiteral("version")] != QLatin1String("1.0")) {
120         setErrorMsg(QStringLiteral("Unknown version of PKey Authentication: %1").arg(params[QStringLiteral("version")]));
121         return QByteArray();
122     }
123 
124     const auto authoritiesInfo = parseCertSubjectInfo(params[QStringLiteral("certauthorities")]);
125 
126     QCA::ConvertResult importResult;
127     const QCA::CertificateCollection certs = QCA::CertificateCollection::fromFlatTextFile(mCertFile, &importResult);
128 
129     if (importResult != QCA::ConvertGood) {
130         setErrorMsg(QStringLiteral("Certificate import failed"));
131         return QByteArray();
132     }
133 
134     QCA::Certificate cert;
135     const auto certificates = certs.certificates();
136     for (const auto &c : certificates) {
137         if (c.issuerInfo() == authoritiesInfo) {
138             cert = c;
139             break;
140         }
141     }
142 
143     if (cert.isNull()) {
144         setErrorMsg(QStringLiteral("No suitable certificate found"));
145         return QByteArray();
146     }
147 
148     QCA::PrivateKey privateKey = QCA::PrivateKey::fromPEMFile(mKeyFile, mKeyPassword.toUtf8(), &importResult);
149     if (importResult != QCA::ConvertGood) {
150         setErrorMsg(QStringLiteral("Private key import failed"));
151         return QByteArray();
152     }
153 
154     const QString certStr = escapeSlashes(QString::fromLatin1(cert.toDER().toBase64()));
155     const QString header = QStringLiteral("{\"x5c\":[\"%1\"],\"typ\":\"JWT\",\"alg\":\"RS256\"}").arg(certStr);
156 
157     const QString payload = QStringLiteral("{\"nonce\":\"%1\",\"iat\":\"%2\",\"aud\":\"%3\"}")
158                                 .arg(params[QStringLiteral("nonce")])
159                                 .arg(QDateTime::currentSecsSinceEpoch())
160                                 .arg(escapeSlashes(params[QStringLiteral("submiturl")]));
161 
162     const auto headerB64 = header.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
163     const auto payloadB64 = payload.toUtf8().toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
164 
165     QCA::SecureArray data(headerB64 + '.' + payloadB64);
166 
167     QByteArray sig = privateKey.signMessage(data, QCA::EMSA3_SHA256, QCA::IEEE_1363);
168 
169     return headerB64 + '.' + payloadB64 + '.' + sig.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
170 }
171 
resultUri() const172 const QUrl &EwsPKeyAuthJob::resultUri() const
173 {
174     return mResultUri;
175 }
176 
getAuthHeader()177 QString EwsPKeyAuthJob::getAuthHeader()
178 {
179     const QUrlQuery query(mPKeyUri);
180     QMap<QString, QString> params;
181     for (const auto &it : query.queryItems()) {
182         params[it.first.toLower()] = QUrl::fromPercentEncoding(it.second.toLatin1());
183     }
184 
185     if (params.contains(QStringLiteral("submiturl")) && params.contains(QStringLiteral("nonce")) && params.contains(QStringLiteral("certauthorities"))
186         && params.contains(QStringLiteral("context")) && params.contains(QStringLiteral("version"))) {
187         const auto respToken = buildAuthResponse(params);
188 
189         if (!respToken.isEmpty()) {
190             return QLatin1String("PKeyAuth AuthToken=\"%1\",Context=\"%2\",Version=\"1.0\"")
191                 .arg(QString::fromLatin1(respToken), params[QStringLiteral("context")]);
192         } else {
193             return {};
194         }
195     } else {
196         return {};
197     }
198 }
199