1 /*
2 * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * for more details.
13 */
14
15 #include "sslbutton.h"
16 #include "account.h"
17 #include "accountstate.h"
18 #include "theme.h"
19
20 #include <QMenu>
21 #include <QUrl>
22 #include <QtNetwork>
23 #include <QSslConfiguration>
24 #include <QWidgetAction>
25 #include <QLabel>
26
27 namespace OCC {
28
29 Q_LOGGING_CATEGORY(lcSsl, "nextcloud.gui.ssl", QtInfoMsg)
30
SslButton(QWidget * parent)31 SslButton::SslButton(QWidget *parent)
32 : QToolButton(parent)
33 {
34 setPopupMode(QToolButton::InstantPopup);
35 setAutoRaise(true);
36
37 _menu = new QMenu(this);
38 QObject::connect(_menu, &QMenu::aboutToShow,
39 this, &SslButton::slotUpdateMenu);
40 setMenu(_menu);
41 }
42
addCertDetailsField(const QString & key,const QString & value)43 static QString addCertDetailsField(const QString &key, const QString &value)
44 {
45 if (value.isEmpty())
46 return QString();
47
48 return QLatin1String("<tr><td style=\"vertical-align: top;\"><b>") + key
49 + QLatin1String("</b></td><td style=\"vertical-align: bottom;\">") + value
50 + QLatin1String("</td></tr>");
51 }
52
53
54 // necessary indication only, not sufficient for primary validation!
isSelfSigned(const QSslCertificate & certificate)55 static bool isSelfSigned(const QSslCertificate &certificate)
56 {
57 return certificate.issuerInfo(QSslCertificate::CommonName) == certificate.subjectInfo(QSslCertificate::CommonName)
58 && certificate.issuerInfo(QSslCertificate::OrganizationalUnitName) == certificate.subjectInfo(QSslCertificate::OrganizationalUnitName);
59 }
60
buildCertMenu(QMenu * parent,const QSslCertificate & cert,const QList<QSslCertificate> & userApproved,int pos,const QList<QSslCertificate> & systemCaCertificates)61 QMenu *SslButton::buildCertMenu(QMenu *parent, const QSslCertificate &cert,
62 const QList<QSslCertificate> &userApproved, int pos, const QList<QSslCertificate> &systemCaCertificates)
63 {
64 QString cn = QStringList(cert.subjectInfo(QSslCertificate::CommonName)).join(QChar(';'));
65 QString ou = QStringList(cert.subjectInfo(QSslCertificate::OrganizationalUnitName)).join(QChar(';'));
66 QString org = QStringList(cert.subjectInfo(QSslCertificate::Organization)).join(QChar(';'));
67 QString country = QStringList(cert.subjectInfo(QSslCertificate::CountryName)).join(QChar(';'));
68 QString state = QStringList(cert.subjectInfo(QSslCertificate::StateOrProvinceName)).join(QChar(';'));
69 QString issuer = QStringList(cert.issuerInfo(QSslCertificate::CommonName)).join(QChar(';'));
70 if (issuer.isEmpty())
71 issuer = QStringList(cert.issuerInfo(QSslCertificate::OrganizationalUnitName)).join(QChar(';'));
72 QString sha1 = Utility::formatFingerprint(cert.digest(QCryptographicHash::Sha1).toHex(), false);
73 QByteArray sha265hash = cert.digest(QCryptographicHash::Sha256).toHex();
74 QString sha256escaped =
75 Utility::escape(Utility::formatFingerprint(sha265hash.left(sha265hash.length() / 2), false))
76 + QLatin1String("<br/>")
77 + Utility::escape(Utility::formatFingerprint(sha265hash.mid(sha265hash.length() / 2), false));
78 QString serial = QString::fromUtf8(cert.serialNumber());
79 QString effectiveDate = cert.effectiveDate().date().toString();
80 QString expiryDate = cert.expiryDate().date().toString();
81 QString sna = QStringList(cert.subjectAlternativeNames().values()).join(" ");
82
83 QString details;
84 QTextStream stream(&details);
85
86 stream << QLatin1String("<html><body>");
87
88 stream << tr("<h3>Certificate Details</h3>");
89
90 stream << QLatin1String("<table>");
91 stream << addCertDetailsField(tr("Common Name (CN):"), Utility::escape(cn));
92 stream << addCertDetailsField(tr("Subject Alternative Names:"), Utility::escape(sna).replace(" ", "<br/>"));
93 stream << addCertDetailsField(tr("Organization (O):"), Utility::escape(org));
94 stream << addCertDetailsField(tr("Organizational Unit (OU):"), Utility::escape(ou));
95 stream << addCertDetailsField(tr("State/Province:"), Utility::escape(state));
96 stream << addCertDetailsField(tr("Country:"), Utility::escape(country));
97 stream << addCertDetailsField(tr("Serial:"), Utility::escape(serial));
98 stream << QLatin1String("</table>");
99
100 stream << tr("<h3>Issuer</h3>");
101
102 stream << QLatin1String("<table>");
103 stream << addCertDetailsField(tr("Issuer:"), Utility::escape(issuer));
104 stream << addCertDetailsField(tr("Issued on:"), Utility::escape(effectiveDate));
105 stream << addCertDetailsField(tr("Expires on:"), Utility::escape(expiryDate));
106 stream << QLatin1String("</table>");
107
108 stream << tr("<h3>Fingerprints</h3>");
109
110 stream << QLatin1String("<table>");
111
112 stream << addCertDetailsField(tr("SHA-256:"), sha256escaped);
113 stream << addCertDetailsField(tr("SHA-1:"), Utility::escape(sha1));
114 stream << QLatin1String("</table>");
115
116 if (userApproved.contains(cert)) {
117 stream << tr("<p><b>Note:</b> This certificate was manually approved</p>");
118 }
119 stream << QLatin1String("</body></html>");
120
121 QString txt;
122 if (pos > 0) {
123 txt += QString(2 * pos, ' ');
124 if (!Utility::isWindows()) {
125 // doesn't seem to work reliably on Windows
126 txt += QChar(0x21AA); // nicer '->' symbol
127 txt += QChar(' ');
128 }
129 }
130
131 QString certId = cn.isEmpty() ? ou : cn;
132
133 if (systemCaCertificates.contains(cert)) {
134 txt += certId;
135 } else {
136 if (isSelfSigned(cert)) {
137 txt += tr("%1 (self-signed)").arg(certId);
138 } else {
139 txt += tr("%1").arg(certId);
140 }
141 }
142
143 // create label first
144 auto *label = new QLabel(parent);
145 label->setStyleSheet(QLatin1String("QLabel { padding: 8px; }"));
146 label->setTextFormat(Qt::RichText);
147 label->setText(details);
148
149 // plug label into widget action
150 auto *action = new QWidgetAction(parent);
151 action->setDefaultWidget(label);
152 // plug action into menu
153 auto *menu = new QMenu(parent);
154 menu->menuAction()->setText(txt);
155 menu->addAction(action);
156
157 return menu;
158 }
159
updateAccountState(AccountState * accountState)160 void SslButton::updateAccountState(AccountState *accountState)
161 {
162 if (!accountState || !accountState->isConnected()) {
163 setVisible(false);
164 return;
165 } else {
166 setVisible(true);
167 }
168 _accountState = accountState;
169
170 AccountPtr account = _accountState->account();
171 if (account->url().scheme() == QLatin1String("https")) {
172 setIcon(QIcon(QLatin1String(":/client/theme/lock-https.svg")));
173 QSslCipher cipher = account->_sessionCipher;
174 setToolTip(tr("This connection is encrypted using %1 bit %2.\n").arg(cipher.usedBits()).arg(cipher.name()));
175 } else {
176 setIcon(QIcon(QLatin1String(":/client/theme/lock-http.svg")));
177 setToolTip(tr("This connection is NOT secure as it is not encrypted.\n"));
178 }
179 }
180
slotUpdateMenu()181 void SslButton::slotUpdateMenu()
182 {
183 _menu->clear();
184
185 if (!_accountState) {
186 return;
187 }
188
189 AccountPtr account = _accountState->account();
190
191 _menu->addAction(tr("Server version: %1").arg(account->serverVersion()))->setEnabled(false);
192
193 if (account->isHttp2Supported()) {
194 _menu->addAction("HTTP/2")->setEnabled(false);
195 }
196
197 if (account->url().scheme() == QLatin1String("https")) {
198 QString sslVersion = account->_sessionCipher.protocolString()
199 + ", " + account->_sessionCipher.authenticationMethod()
200 + ", " + account->_sessionCipher.keyExchangeMethod()
201 + ", " + account->_sessionCipher.encryptionMethod();
202 _menu->addAction(sslVersion)->setEnabled(false);
203
204 if (account->_sessionTicket.isEmpty()) {
205 _menu->addAction(tr("No support for SSL session tickets/identifiers"))->setEnabled(false);
206 }
207
208 QList<QSslCertificate> chain = account->_peerCertificateChain;
209
210 if (chain.isEmpty()) {
211 qCWarning(lcSsl) << "Empty certificate chain";
212 return;
213 }
214
215 _menu->addAction(tr("Certificate information:"))->setEnabled(false);
216
217 const auto systemCerts = QSslConfiguration::systemCaCertificates();
218
219 QList<QSslCertificate> tmpChain;
220 foreach (QSslCertificate cert, chain) {
221 tmpChain << cert;
222 if (systemCerts.contains(cert))
223 break;
224 }
225 chain = tmpChain;
226
227 // find trust anchor (informational only, verification is done by QSslSocket!)
228 for (const QSslCertificate &rootCA : systemCerts) {
229 if (rootCA.issuerInfo(QSslCertificate::CommonName) == chain.last().issuerInfo(QSslCertificate::CommonName)
230 && rootCA.issuerInfo(QSslCertificate::Organization) == chain.last().issuerInfo(QSslCertificate::Organization)) {
231 chain.append(rootCA);
232 break;
233 }
234 }
235
236 QListIterator<QSslCertificate> it(chain);
237 it.toBack();
238 int i = 0;
239 while (it.hasPrevious()) {
240 _menu->addMenu(buildCertMenu(_menu, it.previous(), account->approvedCerts(), i, systemCerts));
241 i++;
242 }
243 } else {
244 _menu->addAction(tr("The connection is not secure"))->setEnabled(false);
245 }
246 }
247
248 } // namespace OCC
249