KHealthCertificate

eudgcparser.cpp
1/*
2 * SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
3 * SPDX-License-Identifier: LGPL-2.0-or-later
4 */
5
6#include "eudgcparser_p.h"
7#include "cborutils_p.h"
8#include "coseparser_p.h"
9#include "logging.h"
10#include "zlib/zlib_p.h"
11
12#include <KCodecs>
13
14#include <QCborStreamReader>
15#include <QDebug>
16#include <QFile>
17#include <QJsonDocument>
18#include <QJsonObject>
19#include <QLocale>
20#include <QVariant>
21
22// std::variant visitor skipping std::monostate alternative
23template <typename T>
24struct visitor {
25 visitor(T _f) : func(_f) {}
26 void operator()(std::monostate) {};
27 template <typename Arg>
28 void operator()(Arg &a) { func(a); }
29
30 T func;
31};
32
33EuDgcParser::EuDgcParser() = default;
34EuDgcParser::~EuDgcParser() = default;
35
36void EuDgcParser::init()
37{
38 Q_INIT_RESOURCE(eu_dgc_data);
39 Q_INIT_RESOURCE(eu_dgc_certs);
40}
41
42static QString translateValue(const QString &type, const QString &key)
43{
44 QFile f(QLatin1String(":/org.kde.khealthcertificate/eu-dgc/") + type + QLatin1String(".json"));
45 if (!f.open(QFile::ReadOnly)) {
46 qCWarning(Log) << "no translation table found for" << type;
47 return key;
48 }
49
50 const auto obj = QJsonDocument::fromJson(f.readAll()).object();
51 const auto language = QLocale().name().left(QLocale().name().indexOf(QLatin1Char('_')));
52 auto it = obj.constFind(key + QLatin1Char('[') + language + QLatin1Char(']'));
53 if (it != obj.constEnd()) {
54 return it.value().toString();
55 }
56 it = obj.constFind(key);
57 if (it != obj.constEnd()) {
58 return it.value().toString();
59 }
60 return key;
61}
62
63QVariant EuDgcParser::parse(const QByteArray &data) const
64{
65 if (!data.startsWith("HC1:") && !data.startsWith("DK3:")) {
66 return {};
67 }
68
69 const auto decoded = Zlib::decompressZlib(KCodecs::base45Decode(data.mid(4)));
70 if (decoded.isEmpty()) {
71 return {};
72 }
73
74 CoseParser cose;
75 cose.parse(decoded);
76 if (cose.payload().isEmpty()) {
77 return {};
78 }
79
80 QCborStreamReader reader(cose.payload());
81 if (!reader.isMap()) {
82 return {};
83 }
84 reader.enterContainer();
85 // parse certificate header
86 QDateTime issueDt, expiryDt;
87 while (reader.hasNext()) {
88 const auto key = CborUtils::readInteger(reader);
89 switch (key) {
90 case -260:
91 parseCertificate(reader);
92 break;
93 case 1:
94 qCDebug(Log) << "key issuer:" << CborUtils::readString(reader);
95 break;
96 case 4:
97 expiryDt = QDateTime::fromSecsSinceEpoch(CborUtils::readInteger(reader));
98 break;
99 case 6:
100 issueDt = QDateTime::fromSecsSinceEpoch(CborUtils::readInteger(reader));
101 break;
102 default:
103 qCDebug(Log) << "unhandled header key:" << key;
104 reader.next();
105 }
106 }
107 reader.leaveContainer();
108 std::visit(visitor([&issueDt](auto &cert) { cert.setCertificateIssueDate(issueDt); }), m_cert);
109 std::visit(visitor([&expiryDt](auto &cert) { cert.setCertificateExpiryDate(expiryDt); }), m_cert);
110
111 // signature validation
112 auto sigState = cose.signatureState();
113 if (sigState == CoseParser::ValidSignature && cose.certificate().expiryDate() < issueDt ) {
114 sigState = CoseParser::InvalidSignature;
115 }
116 // TODO check key usage OIDs for 1.3.6.1.4.1.1847.2021.1.[1-3] / 1.3.6.1.4.1.0.1847.2021.1.[1-3]
117 // (seems unused so far?)
118 switch (sigState) {
119 case CoseParser::InvalidSignature:
120 std::visit(visitor([](auto &cert) { cert.setSignatureState(KHealthCertificate::InvalidSignature); }), m_cert);
121 break;
122 case CoseParser::ValidSignature:
123 std::visit(visitor([](auto &cert) { cert.setSignatureState(KHealthCertificate::ValidSignature); }), m_cert);
124 break;
125 default:
126 std::visit(visitor([](auto &cert) { cert.setSignatureState(KHealthCertificate::UnknownSignature); }), m_cert);
127 break;
128 }
129 std::visit(visitor([&data](auto &cert) { cert.setRawData(data); }), m_cert);
130 return std::visit([](const auto &cert) { return QVariant::fromValue(cert); }, m_cert);
131}
132
133void EuDgcParser::parseCertificate(QCborStreamReader &reader) const
134{
135 if (!reader.isMap()) {
136 return;
137 }
138 reader.enterContainer();
139 const auto version = CborUtils::readInteger(reader);
140 if (version != 1) {
141 qCWarning(Log) << "unknown EU DGC version:" << version;
142 return;
143 }
144
145 parseCertificateV1(reader);
146}
147
148void EuDgcParser::parseCertificateV1(QCborStreamReader &reader) const
149{
150 if (!reader.isMap()) {
151 return;
152 }
153
155 QDate dob;
156
157 reader.enterContainer();
158 while (reader.hasNext()) {
159 const auto key = CborUtils::readString(reader);
160 if (key == QLatin1String("v")) {
161 parseCertificateArray(reader, &EuDgcParser::parseVaccinationCertificate);
162 } else if (key == QLatin1String("t")) {
163 parseCertificateArray(reader, &EuDgcParser::parseTestCertificate);
164 } else if (key == QLatin1String("r")) {
165 parseCertificateArray(reader, &EuDgcParser::parseRecoveryCertificate);
166 } else if (key == QLatin1String("nam")) {
167 name = parseName(reader);
168 } else if (key == QLatin1String("dob")) {
169 dob = QDate::fromString(CborUtils::readString(reader), Qt::ISODate);
170 } else {
171 qCDebug(Log) << "unhandled element:" << key;
172 reader.next();
173 }
174 }
175 reader.leaveContainer();
176
177 std::visit(visitor([&name](auto &cert) { cert.setName(name); }), m_cert);
178 std::visit(visitor([&dob](auto &cert) { cert.setDateOfBirth(dob); }), m_cert);
179}
180
181void EuDgcParser::parseCertificateArray(QCborStreamReader &reader, void (EuDgcParser::*func)(QCborStreamReader&) const) const
182{
183 if (!reader.isArray() || !std::holds_alternative<std::monostate>(m_cert)) {
184 return;
185 }
186 reader.enterContainer();
187 while (reader.hasNext()) {
188 (this->*func)(reader);
189 }
190 reader.leaveContainer();
191}
192
193void EuDgcParser::parseVaccinationCertificate(QCborStreamReader& reader) const
194{
195 if (!reader.isMap()) {
196 return;
197 }
199 reader.enterContainer();
200 while (reader.hasNext()) {
201 const auto key = CborUtils::readString(reader);
202 if (key == QLatin1String("tg")) {
203 cert.setDisease(translateValue(key, CborUtils::readString(reader)));
204 } else if (key == QLatin1String("vp")) {
205 cert.setVaccineType(translateValue(key, CborUtils::readString(reader)));
206 } else if (key == QLatin1String("dt")) {
207 cert.setDate(QDate::fromString(CborUtils::readString(reader), Qt::ISODate));
208 } else if (key == QLatin1String("mp")) {
209 const auto productId = CborUtils::readString(reader);
210 cert.setVaccine(translateValue(key,productId));
211 if (productId.startsWith(QLatin1String("EU/")) && productId.count(QLatin1Char('/')) == 3) {
212 const auto num = QStringView(productId).mid(productId.lastIndexOf(QLatin1Char('/')) + 1);
213 cert.setVaccineUrl(QUrl(QLatin1String("https://ec.europa.eu/health/documents/community-register/html/h") + num + QLatin1String(".htm")));
214 }
215 } else if (key == QLatin1String("ma")) {
216 cert.setManufacturer(translateValue(key, CborUtils::readString(reader)));
217 } else if (key == QLatin1String("dn")) {
218 cert.setDose(CborUtils::readInteger(reader));
219 } else if (key == QLatin1String("sd")) {
220 cert.setTotalDoses(CborUtils::readInteger(reader));
221 } else if (key == QLatin1String("co")) {
222 cert.setCountry(CborUtils::readString(reader));
223 } else if (key == QLatin1String("is")) {
224 cert.setCertificateIssuer(CborUtils::readString(reader));
225 } else if (key == QLatin1String("ci")) {
226 cert.setCertificateId(CborUtils::readString(reader));
227 } else {
228 qCDebug(Log) << "unhandled vaccine key:" << key;
229 reader.next();
230 }
231 }
232 reader.leaveContainer();
233 m_cert = std::move(cert);
234}
235
236void EuDgcParser::parseTestCertificate(QCborStreamReader &reader) const
237{
238 if (!reader.isMap()) {
239 return;
240 }
241 KTestCertificate cert;
242 reader.enterContainer();
243 while (reader.hasNext()) {
244 const auto key = CborUtils::readString(reader);
245 if (key == QLatin1String("tg")) {
246 cert.setDisease(translateValue(key, CborUtils::readString(reader)));
247 } else if (key == QLatin1String("tt")) {
248 cert.setTestType(translateValue(QLatin1String("tcTt"), CborUtils::readString(reader)));
249 } else if (key == QLatin1String("nm")) {
250 cert.setTestName(CborUtils::readString(reader));
251 } else if (key == QLatin1String("ma")) {
252 const auto productId = CborUtils::readString(reader);
253 cert.setTestName(translateValue(QLatin1String("tcMa"), productId));
254 cert.setTestUrl(QUrl(QLatin1String("https://covid-19-diagnostics.jrc.ec.europa.eu/devices/detail/") + productId));
255 } else if (key == QLatin1String("sc")) {
256 cert.setDate(QDate::fromString(CborUtils::readString(reader), Qt::ISODate));
257 } else if (key == QLatin1String("tr")) {
258 const auto value = CborUtils::readString(reader);
259 cert.setResultString(translateValue(QLatin1String("tcTr"), value));
260 cert.setResult(value == QLatin1String("260415000") ? KTestCertificate::Negative : KTestCertificate::Positive);
261 } else if (key == QLatin1String("tc")) {
262 cert.setTestCenter(CborUtils::readString(reader));
263 } else if (key == QLatin1String("co")) {
264 cert.setCountry(CborUtils::readString(reader));
265 } else if (key == QLatin1String("is")) {
266 cert.setCertificateIssuer(CborUtils::readString(reader));
267 } else if (key == QLatin1String("ci")) {
268 cert.setCertificateId(CborUtils::readString(reader));
269 } else {
270 qCDebug(Log) << "unhandled test key:" << key;
271 reader.next();
272 }
273 }
274 reader.leaveContainer();
275 m_cert = std::move(cert);
276}
277
278void EuDgcParser::parseRecoveryCertificate(QCborStreamReader &reader) const
279{
280 if (!reader.isMap()) {
281 return;
282 }
284 reader.enterContainer();
285 while (reader.hasNext()) {
286 const auto key = CborUtils::readString(reader);
287 if (key == QLatin1String("tg")) {
288 cert.setDisease(translateValue(key, CborUtils::readString(reader)));
289 } else if (key == QLatin1String("fr")) {
290 cert.setDateOfPositiveTest(QDate::fromString(CborUtils::readString(reader), Qt::ISODate));
291 } else if (key == QLatin1String("df")) {
292 cert.setValidFrom(QDate::fromString(CborUtils::readString(reader), Qt::ISODate));
293 } else if (key == QLatin1String("du")) {
294 cert.setValidUntil(QDate::fromString(CborUtils::readString(reader), Qt::ISODate));
295 } else if (key == QLatin1String("is")) {
296 cert.setCertificateIssuer(CborUtils::readString(reader));
297 } else if (key == QLatin1String("ci")) {
298 cert.setCertificateId(CborUtils::readString(reader));
299 } else {
300 qCDebug(Log) << "unhandled recovery key:" << key;
301 reader.next();
302 }
303 }
304 reader.leaveContainer();
305 m_cert = std::move(cert);
306}
307
308QString EuDgcParser::parseName(QCborStreamReader &reader) const
309{
310 if (!reader.isMap()) {
311 return {};
312 }
313 QString fn, gn;
314 reader.enterContainer();
315 while (reader.hasNext()) {
316 const auto key = CborUtils::readString(reader);
317 if (key == QLatin1String("fn")) {
318 fn = CborUtils::readString(reader);
319 } else if (key == QLatin1String("gn")) {
320 gn = CborUtils::readString(reader);
321 } else {
322 reader.next();
323 }
324 }
325 reader.leaveContainer();
326
327 return gn + QLatin1Char(' ') + fn;
328}
@ ValidSignature
signature is valid
@ InvalidSignature
signature is invalid
@ UnknownSignature
signature verification was attempted but didn't yield a result, e.g. due to a missing certificate of ...
A recovery certificate.
A test certificate.
A vaccination certificate.
Type type(const QSqlDatabase &db)
KCODECS_EXPORT QByteArray base45Decode(QByteArrayView in)
KDB_EXPORT KDbVersionInfo version()
QString name(StandardAction id)
QByteArray mid(qsizetype pos, qsizetype len) const const
bool startsWith(QByteArrayView bv) const const
bool hasNext() const const
bool isArray() const const
bool isMap() const const
bool next(int maxRecursion)
QDate fromString(QStringView string, QStringView format, QCalendar cal)
QDateTime fromSecsSinceEpoch(qint64 secs)
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
QString name() const const
QString left(qsizetype n) const const
QStringView mid(qsizetype start, qsizetype length) const const
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:13:32 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.