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