1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2007, 2008, 2010 Andreas Hartmetz <ahartmetz@gmail.com> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | |
8 | #include "kssld.h" |
9 | |
10 | #include "ksslcertificatemanager.h" |
11 | #include "ksslcertificatemanager_p.h" |
12 | #include "kssld_adaptor.h" |
13 | |
14 | #include <KConfig> |
15 | #include <KConfigGroup> |
16 | |
17 | #include <KPluginFactory> |
18 | #include <QDate> |
19 | |
20 | K_PLUGIN_CLASS_WITH_JSON(KSSLD, "kssld.json" ) |
21 | |
22 | class KSSLDPrivate |
23 | { |
24 | public: |
25 | KSSLDPrivate() |
26 | : config(QStringLiteral("ksslcertificatemanager" ), KConfig::SimpleConfig) |
27 | { |
28 | struct strErr { |
29 | const char *str; |
30 | QSslError::SslError err; |
31 | }; |
32 | |
33 | // hmmm, looks like these are all of the errors where it is possible to continue. |
34 | // TODO for Qt > 5.14 QSslError::SslError is a Q_ENUM, and we can therefore replace this manual mapping table |
35 | const static strErr strError[] = {{.str: "NoError" , .err: QSslError::NoError}, |
36 | {.str: "UnknownError" , .err: QSslError::UnspecifiedError}, |
37 | {.str: "InvalidCertificateAuthority" , .err: QSslError::InvalidCaCertificate}, |
38 | {.str: "InvalidCertificate" , .err: QSslError::UnableToDecodeIssuerPublicKey}, |
39 | {.str: "CertificateSignatureFailed" , .err: QSslError::CertificateSignatureFailed}, |
40 | {.str: "SelfSignedCertificate" , .err: QSslError::SelfSignedCertificate}, |
41 | {.str: "RevokedCertificate" , .err: QSslError::CertificateRevoked}, |
42 | {.str: "InvalidCertificatePurpose" , .err: QSslError::InvalidPurpose}, |
43 | {.str: "RejectedCertificate" , .err: QSslError::CertificateRejected}, |
44 | {.str: "UntrustedCertificate" , .err: QSslError::CertificateUntrusted}, |
45 | {.str: "ExpiredCertificate" , .err: QSslError::CertificateExpired}, |
46 | {.str: "HostNameMismatch" , .err: QSslError::HostNameMismatch}, |
47 | {.str: "UnableToGetLocalIssuerCertificate" , .err: QSslError::UnableToGetLocalIssuerCertificate}, |
48 | {.str: "InvalidNotBeforeField" , .err: QSslError::InvalidNotBeforeField}, |
49 | {.str: "InvalidNotAfterField" , .err: QSslError::InvalidNotAfterField}, |
50 | {.str: "CertificateNotYetValid" , .err: QSslError::CertificateNotYetValid}, |
51 | {.str: "SubjectIssuerMismatch" , .err: QSslError::SubjectIssuerMismatch}, |
52 | {.str: "AuthorityIssuerSerialNumberMismatch" , .err: QSslError::AuthorityIssuerSerialNumberMismatch}, |
53 | {.str: "SelfSignedCertificateInChain" , .err: QSslError::SelfSignedCertificateInChain}, |
54 | {.str: "UnableToVerifyFirstCertificate" , .err: QSslError::UnableToVerifyFirstCertificate}, |
55 | {.str: "UnableToDecryptCertificateSignature" , .err: QSslError::UnableToDecryptCertificateSignature}, |
56 | {.str: "UnableToGetIssuerCertificate" , .err: QSslError::UnableToGetIssuerCertificate}}; |
57 | |
58 | for (const strErr &row : strError) { |
59 | QString s = QString::fromLatin1(ba: row.str); |
60 | stringToSslError.insert(key: s, value: row.err); |
61 | sslErrorToString.insert(key: row.err, value: s); |
62 | } |
63 | } |
64 | |
65 | KConfig config; |
66 | QHash<QString, QSslError::SslError> stringToSslError; |
67 | QHash<QSslError::SslError, QString> sslErrorToString; |
68 | }; |
69 | |
70 | KSSLD::KSSLD(QObject *parent, const QVariantList &) |
71 | : KDEDModule(parent) |
72 | , d(new KSSLDPrivate()) |
73 | { |
74 | new KSSLDAdaptor(this); |
75 | pruneExpiredRules(); |
76 | } |
77 | |
78 | KSSLD::~KSSLD() = default; |
79 | |
80 | void KSSLD::setRule(const KSslCertificateRule &rule) |
81 | { |
82 | if (rule.hostName().isEmpty()) { |
83 | return; |
84 | } |
85 | KConfigGroup group = d->config.group(group: QString::fromLatin1(ba: rule.certificate().digest().toHex())); |
86 | |
87 | QStringList sl; |
88 | |
89 | QString dtString = QStringLiteral("ExpireUTC " ); |
90 | dtString.append(s: rule.expiryDateTime().toString(format: Qt::ISODate)); |
91 | sl.append(t: dtString); |
92 | |
93 | if (rule.isRejected()) { |
94 | sl.append(QStringLiteral("Reject" )); |
95 | } else { |
96 | const auto ignoredErrors = rule.ignoredErrors(); |
97 | for (QSslError::SslError e : ignoredErrors) { |
98 | sl.append(t: d->sslErrorToString.value(key: e)); |
99 | } |
100 | } |
101 | |
102 | if (!group.hasKey(key: "CertificatePEM" )) { |
103 | group.writeEntry(key: "CertificatePEM" , value: rule.certificate().toPem()); |
104 | } |
105 | #ifdef PARANOIA |
106 | else if (group.readEntry("CertificatePEM" ) != rule.certificate().toPem()) { |
107 | return; |
108 | } |
109 | #endif |
110 | group.writeEntry(key: rule.hostName(), value: sl); |
111 | group.sync(); |
112 | } |
113 | |
114 | void KSSLD::clearRule(const KSslCertificateRule &rule) |
115 | { |
116 | clearRule(cert: rule.certificate(), hostName: rule.hostName()); |
117 | } |
118 | |
119 | void KSSLD::clearRule(const QSslCertificate &cert, const QString &hostName) |
120 | { |
121 | KConfigGroup group = d->config.group(group: QString::fromLatin1(ba: cert.digest().toHex())); |
122 | group.deleteEntry(pKey: hostName); |
123 | if (group.keyList().size() < 2) { |
124 | group.deleteGroup(); |
125 | } |
126 | group.sync(); |
127 | } |
128 | |
129 | void KSSLD::pruneExpiredRules() |
130 | { |
131 | // expired rules are deleted when trying to load them, so we just try to load all rules. |
132 | // be careful about iterating over KConfig(Group) while changing it |
133 | const QStringList groupNames = d->config.groupList(); |
134 | for (const QString &groupName : groupNames) { |
135 | QByteArray certDigest = groupName.toLatin1(); |
136 | const QStringList keys = d->config.group(group: groupName).keyList(); |
137 | for (const QString &key : keys) { |
138 | if (key == QLatin1String("CertificatePEM" )) { |
139 | continue; |
140 | } |
141 | KSslCertificateRule r = rule(cert: QSslCertificate(certDigest), hostName: key); |
142 | } |
143 | } |
144 | } |
145 | |
146 | // check a domain name with subdomains for well-formedness and count the dot-separated parts |
147 | static QString normalizeSubdomains(const QString &hostName, int *namePartsCount) |
148 | { |
149 | QString ret; |
150 | int partsCount = 0; |
151 | bool wasPrevDot = true; // -> allow no dot at the beginning and count first name part |
152 | const int length = hostName.length(); |
153 | for (int i = 0; i < length; i++) { |
154 | const QChar c = hostName.at(i); |
155 | if (c == QLatin1Char('.')) { |
156 | if (wasPrevDot || (i + 1 == hostName.length())) { |
157 | // consecutive dots or a dot at the end are forbidden |
158 | partsCount = 0; |
159 | ret.clear(); |
160 | break; |
161 | } |
162 | wasPrevDot = true; |
163 | } else { |
164 | if (wasPrevDot) { |
165 | partsCount++; |
166 | } |
167 | wasPrevDot = false; |
168 | } |
169 | ret.append(c); |
170 | } |
171 | |
172 | *namePartsCount = partsCount; |
173 | return ret; |
174 | } |
175 | |
176 | KSslCertificateRule KSSLD::rule(const QSslCertificate &cert, const QString &hostName) const |
177 | { |
178 | const QByteArray certDigest = cert.digest().toHex(); |
179 | KConfigGroup group = d->config.group(group: QString::fromLatin1(ba: certDigest)); |
180 | |
181 | KSslCertificateRule ret(cert, hostName); |
182 | bool foundHostName = false; |
183 | |
184 | int needlePartsCount; |
185 | QString needle = normalizeSubdomains(hostName, namePartsCount: &needlePartsCount); |
186 | |
187 | // Find a rule for the hostname, either... |
188 | if (group.hasKey(key: needle)) { |
189 | // directly (host, site.tld, a.site.tld etc) |
190 | if (needlePartsCount >= 1) { |
191 | foundHostName = true; |
192 | } |
193 | } else { |
194 | // or with wildcards |
195 | // "tld" <- "*." and "site.tld" <- "*.tld" are not valid matches, |
196 | // "a.site.tld" <- "*.site.tld" is |
197 | while (--needlePartsCount >= 2) { |
198 | const int dotIndex = needle.indexOf(c: QLatin1Char('.')); |
199 | Q_ASSERT(dotIndex > 0); // if this fails normalizeSubdomains() failed |
200 | needle.remove(i: 0, len: dotIndex - 1); |
201 | needle[0] = QChar::fromLatin1(c: '*'); |
202 | if (group.hasKey(key: needle)) { |
203 | foundHostName = true; |
204 | break; |
205 | } |
206 | needle.remove(i: 0, len: 2); // remove "*." |
207 | } |
208 | } |
209 | |
210 | if (!foundHostName) { |
211 | // Don't make a rule with the failed wildcard pattern - use the original hostname. |
212 | return KSslCertificateRule(cert, hostName); |
213 | } |
214 | |
215 | // parse entry of the format "ExpireUTC <date>, Reject" or |
216 | //"ExpireUTC <date>, HostNameMismatch, ExpiredCertificate, ..." |
217 | QStringList sl = group.readEntry(key: needle, aDefault: QStringList()); |
218 | |
219 | QDateTime expiryDt; |
220 | // the rule is well-formed if it contains at least the expire date and one directive |
221 | if (sl.size() >= 2) { |
222 | QString dtString = sl.takeFirst(); |
223 | if (dtString.startsWith(s: QLatin1String("ExpireUTC " ))) { |
224 | dtString.remove(i: 0, len: 10 /* length of "ExpireUTC " */); |
225 | expiryDt = QDateTime::fromString(string: dtString, format: Qt::ISODate); |
226 | } |
227 | } |
228 | |
229 | if (!expiryDt.isValid() || expiryDt < QDateTime::currentDateTime()) { |
230 | // the entry is malformed or expired so we remove it |
231 | group.deleteEntry(pKey: needle); |
232 | // the group is useless once only the CertificatePEM entry left |
233 | if (group.keyList().size() < 2) { |
234 | group.deleteGroup(); |
235 | } |
236 | return ret; |
237 | } |
238 | |
239 | QList<QSslError::SslError> ignoredErrors; |
240 | bool isRejected = false; |
241 | for (const QString &s : std::as_const(t&: sl)) { |
242 | if (s == QLatin1String("Reject" )) { |
243 | isRejected = true; |
244 | ignoredErrors.clear(); |
245 | break; |
246 | } |
247 | if (!d->stringToSslError.contains(key: s)) { |
248 | continue; |
249 | } |
250 | ignoredErrors.append(t: d->stringToSslError.value(key: s)); |
251 | } |
252 | |
253 | // Everything is checked and we can make ret valid |
254 | ret.setExpiryDateTime(expiryDt); |
255 | ret.setRejected(isRejected); |
256 | ret.setIgnoredErrors(ignoredErrors); |
257 | return ret; |
258 | } |
259 | |
260 | #include "kssld.moc" |
261 | #include "moc_kssld.cpp" |
262 | #include "moc_kssld_adaptor.cpp" |
263 | |