| 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(ch: 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 | |