KHealthCertificate

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

KDE's Doxygen guidelines are available online.