KHealthCertificate

icaovdsparser.cpp
1/*
2 SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "icaovdsparser_p.h"
7#include "logging.h"
8
9#include <openssl/opensslpp_p.h>
10#include <openssl/verify_p.h>
11#include <openssl/x509loader_p.h>
12
13#include <openssl/x509v3.h>
14
15#include <KTestCertificate>
16#include <KVaccinationCertificate>
17
18#include <KCountry>
19
20#include <QDirIterator>
21#include <QFile>
22#include <QJsonDocument>
23#include <QJsonArray>
24#include <QJsonObject>
25
26void IcaoVdsParser::init()
27{
28 Q_INIT_RESOURCE(icao_csca_certs);
29 Q_INIT_RESOURCE(icaovds_data);
30}
31
32template <typename Cert>
33static void parsePersonalInformation(Cert &cert, const QJsonObject &pidObj)
34{
35 cert.setName(pidObj.value(QLatin1String("n")).toString());
36 cert.setDateOfBirth(QDate::fromString(pidObj.value(QLatin1String("dob")).toString(), Qt::ISODate));
37}
38
39static int jsonValueToInt(const QJsonValue &v)
40{
41 if (v.isDouble()) {
42 return v.toInt();
43 }
44 if (v.isString()) {
45 return v.toString().toInt();
46 }
47 return 0;
48}
49
50static QString lookupDisease(const QString &code)
51{
52 QFile f(QLatin1String(":/org.kde.khealthcertificate/icao/data/diseases.json"));
53 if (!f.open(QFile::ReadOnly)) {
54 qCWarning(Log) << f.fileName() << f.errorString();
55 return code;
56 }
57
58 const auto obj = QJsonDocument::fromJson(f.readAll()).object();
59 const auto name = obj.value(code.left(4)).toString();
60 return name.isEmpty() ? code : name;
61}
62
63static QString lookupVaccine(const QString &code)
64{
65 QFile f(QLatin1String(":/org.kde.khealthcertificate/icao/data/vaccines.json"));
66 if (!f.open(QFile::ReadOnly)) {
67 qCWarning(Log) << f.fileName() << f.errorString();
68 return code;
69 }
70
71 const auto obj = QJsonDocument::fromJson(f.readAll()).object();
72 const auto name = obj.value(code).toString();
73 return name.isEmpty() ? code : name;
74}
75
76static QString alpha3ToAlpha2(const QString &alpha3)
77{
78 const auto c = KCountry::fromAlpha3(alpha3);
79 return c.isValid() ? c.alpha2() : alpha3;
80}
81
82static KHealthCertificate::SignatureValidation verifyCertificate(const openssl::x509_ptr &x509Cert)
83{
84 const auto keyId = X509_get0_authority_key_id(x509Cert.get());
85 if (!keyId) {
87 }
88 const auto keyIdStr = QString::fromUtf8(QByteArray(reinterpret_cast<const char*>(keyId->data), keyId->length).toHex());
89 qCDebug(Log) << keyIdStr;
90
91 // single certificate file for keyId
92 QFile issuerCertFile(QLatin1String(":/org.kde.khealthcertificate/icao/certs/") + keyIdStr + QLatin1String(".der"));
93 if (issuerCertFile.open(QFile::ReadOnly)) {
94 const auto x509IssuerCert = X509Loader::readFromDER(issuerCertFile.readAll());
95 const openssl::evp_pkey_ptr issuerPkey(X509_get_pubkey(x509IssuerCert.get()));
96 const auto certValid = X509_verify(x509Cert.get(), issuerPkey.get());
97 if (certValid == 1) {
99 }
101 }
102
103 // multiple certificates for keyId, try all of them
104 bool foundInvalid = false;
105 for (QDirIterator it(QLatin1String(":/org.kde.khealthcertificate/icao/certs/") + keyIdStr, QDir::Files); it.hasNext();) {
106 QFile issuerCertFile(it.next());
107 if (issuerCertFile.open(QFile::ReadOnly)) {
108 const auto x509IssuerCert = X509Loader::readFromDER(issuerCertFile.readAll());
109 const openssl::evp_pkey_ptr issuerPkey(X509_get_pubkey(x509IssuerCert.get()));
110 const auto certValid = X509_verify(x509Cert.get(), issuerPkey.get());
111 if (certValid == 1) {
113 }
114 foundInvalid = true;
115 } else {
116 qCWarning(Log) << issuerCertFile.fileName() << issuerCertFile.errorString();
117 }
118 }
119
120 if (!foundInvalid) {
121 qCWarning(Log) << "No CSCA certificate found for key id" << keyId;
122 }
124}
125
126QVariant IcaoVdsParser::parse(const QByteArray &data)
127{
128 const auto doc = QJsonDocument::fromJson(data);
129
130 QJsonObject rootObj;
131 if (doc.isObject()) {
132 rootObj = doc.object();
133 } else if (doc.isArray() && doc.array().size() == 1) {
134 rootObj = doc.array().at(0).toObject(); // TODO multiple entries?
135 }
136
137 const auto dataObj = rootObj.value(QLatin1String("data")).toObject();
138 const auto hdrObj = dataObj.value(QLatin1String("hdr")).toObject();
139 const auto msgObj = dataObj.value(QLatin1String("msg")).toObject();
140
141 if (hdrObj.value(QLatin1String("v")).toInt() != 1) {
142 return {};
143 }
144
145 const auto sigObj = rootObj.value(QLatin1String("sig")).toObject();
146
147 // verify certificate used for the signature
148 const auto cert = QByteArray::fromBase64(sigObj.value(QLatin1String("cer")).toString().toUtf8(), QByteArray::Base64UrlEncoding);
149 const uint8_t *certData = reinterpret_cast<const uint8_t*>(cert.data());
150 const openssl::x509_ptr x509Cert(d2i_X509(nullptr, &certData, cert.size()));
151 KHealthCertificate::SignatureValidation sigState = verifyCertificate(x509Cert);
152
153 // verify that the content signature is correct
154 const openssl::evp_pkey_ptr pkey(X509_get_pubkey(x509Cert.get()));
155 const auto alg = sigObj.value(QLatin1String("alg")).toString();
156 const auto signature = QByteArray::fromBase64(sigObj.value(QLatin1String("sigvl")).toString().toUtf8(), QByteArray::Base64UrlEncoding);
157
158 // this need RFC 8785 JSON canonicalization
159 const auto signedData = QJsonDocument(dataObj).toJson(QJsonDocument::Compact);
160
161 bool valid = false;
162 if (alg == QLatin1String("ES256")) {
163 valid = Verify::verifyECDSA(pkey, EVP_sha256(), signedData.constData(), signedData.size(), signature.constData(), signature.size());
164 } else if (alg == QLatin1String("ES384")) {
165 valid = Verify::verifyECDSA(pkey, EVP_sha384(), signedData.constData(), signedData.size(), signature.constData(), signature.size());
166 } else if (alg == QLatin1String("ES512")) {
167 valid = Verify::verifyECDSA(pkey, EVP_sha512(), signedData.constData(), signedData.size(), signature.constData(), signature.size());
168 } else {
169 qCWarning(Log) << "signature algorithm not supported:" << alg;
170 }
171 if (valid && sigState == KHealthCertificate::UncheckedSignature) {
173 }
174
175 const auto type = hdrObj.value(QLatin1String("t")).toString();
176 if (type == QLatin1String("icao.vacc")) {
178 parsePersonalInformation(cert, msgObj.value(QLatin1String("pid")).toObject());
179 cert.setCertificateId(msgObj.value(QLatin1String("uvci")).toString());
180
181 const auto veArray = msgObj.value(QLatin1String("ve")).toArray();
182 if (veArray.isEmpty()) {
183 return {};
184 }
185 for (const auto &veVal : veArray) {
186 const auto veObj = veVal.toObject();
187 cert.setVaccineType(lookupVaccine(veObj.value(QLatin1String("des")).toString()));
188 cert.setDisease(lookupDisease(veObj.value(QLatin1String("dis")).toString()));
189 cert.setVaccine(veObj.value(QLatin1String("nam")).toString());
190
191 const auto vdArray = veObj.value(QLatin1String("vd")).toArray();
192 for (const auto &vdVal : vdArray) {
193 const auto vdObj = vdVal.toObject();
194 const auto seq = jsonValueToInt(vdObj.value(QLatin1String("seq")));
195 if (seq < cert.dose()) {
196 continue;
197 }
198 cert.setDose(seq);
199 cert.setDate(QDate::fromString(vdObj.value(QLatin1String("dvc")).toString(), Qt::ISODate));
200 cert.setCountry(alpha3ToAlpha2(vdObj.value(QLatin1String("ctr")).toString()));
201 }
202 }
203
204 const auto compactData = doc.toJson(QJsonDocument::Compact);
205 cert.setRawData(compactData.size() < data.size() ? compactData : data);
206 cert.setSignatureState(sigState);
207 return cert;
208 }
209
210 if (type == QLatin1String("icao.test")) {
211 KTestCertificate cert;
212 parsePersonalInformation(cert, msgObj.value(QLatin1String("pid")).toObject());
213 cert.setCertificateId(msgObj.value(QLatin1String("utci")).toString());
214
215 const auto spObj = msgObj.value(QLatin1String("sp")).toObject();
216 cert.setTestCenter(spObj.value(QLatin1String("spn")).toString());
217 cert.setCountry(alpha3ToAlpha2(spObj.value(QLatin1String("ctr")).toString()));
218
219 const auto datObj = msgObj.value(QLatin1String("dat")).toObject();
220 cert.setDate(QDateTime::fromString(datObj.value(QLatin1String("sc")).toString(), Qt::ISODate).date());
221
222 const auto trObj = msgObj.value(QLatin1String("tr")).toObject();
223 cert.setTestType(trObj.value(QLatin1String("tc")).toString());
224 const auto result = trObj.value(QLatin1String("r")).toString();
225 cert.setResultString(result);
226 if (result.compare(QLatin1String("negative"), Qt::CaseInsensitive) == 0) {
227 cert.setResult(KTestCertificate::Negative);
228 } else if (result.compare(QLatin1String("positive"), Qt::CaseInsensitive) == 0) {
229 cert.setResult(KTestCertificate::Positive);
230 } else {
231 cert.setResult(KTestCertificate::Unknown);
232 }
233
234 const auto compactData = doc.toJson(QJsonDocument::Compact);
235 cert.setRawData(compactData.size() < data.size() ? compactData : data);
236 cert.setSignatureState(sigState);
237 return cert;
238 }
239
240 return {};
241}
static KCountry fromAlpha3(const char *alpha3Code)
SignatureValidation
Result of attempting to verify the cryptographic signature of a certificate.
@ ValidSignature
signature is valid
@ InvalidSignature
signature is invalid
@ UncheckedSignature
signature verification was not attempted, e.g. as it's not yet implemented for the specific certifica...
@ UnknownSignature
signature verification was attempted but didn't yield a result, e.g. due to a missing certificate of ...
A test certificate.
A vaccination certificate.
Type type(const QSqlDatabase &db)
char * toString(const EngineQuery &query)
QString name(StandardShortcut id)
QByteArray fromBase64(const QByteArray &base64, Base64Options options)
qsizetype size() const const
QByteArray toHex(char separator) const const
QDate fromString(QStringView string, QStringView format, QCalendar cal)
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool hasNext() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
QByteArray toJson(JsonFormat format) const const
QJsonValue value(QLatin1StringView key) const const
bool isDouble() const const
bool isString() const const
int toInt(int defaultValue) const const
QJsonObject toObject() const const
QString toString() const const
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
QString left(qsizetype n) const const
int toInt(bool *ok, int base) const const
CaseInsensitive
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:16:45 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.