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> ¶ms)
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