KHealthCertificate

nlcoronacheckparser.cpp
1/*
2 * SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
3 * SPDX-License-Identifier: LGPL-2.0-or-later
4 */
5
6#include "nlcoronacheckparser_p.h"
7#include "nlbase45_p.h"
8#include "logging.h"
9#include "irmapublickey_p.h"
10#include "irmaverifier_p.h"
11
12#include "openssl/asn1_p.h"
13#include "openssl/bignum_p.h"
14
15#include <KTestCertificate>
16#include <KVaccinationCertificate>
17
18#include <QCryptographicHash>
19#include <QDebug>
20#include <QVariant>
21
22void NLCoronaCheckParser::init()
23{
24 Q_INIT_RESOURCE(nl_public_keys);
25}
26
27static QByteArray nlDecodeAsn1ByteArray(const ASN1::Object &obj)
28{
29 auto it = obj.begin();
30 auto ai = openssl::asn1_integer_ptr(d2i_ASN1_INTEGER(nullptr, &it, obj.size()));
31 if (!ai) {
32 qWarning() << "invalid ASN.1 structure";
33 return {};
34 }
35 auto bn = openssl::bn_ptr(BN_new());
36 BN_zero(bn.get());
37 for (auto i = 0; i < ai->length; ++i) {
38 BN_mul_word(bn.get(), 1 << 8);
39 BN_add_word(bn.get(), ai->data[i]);
40 }
41 BN_div_word(bn.get(), 2);
42 return Bignum::toByteArray(bn);
43}
44
45QVariant NLCoronaCheckParser::parse(const QByteArray &data)
46{
47 if (!data.startsWith("NL2:") || data.size() < 5) {
48 return {};
49 }
50 const auto rawData = NLBase45::decode(data.begin() + 4, data.end());
51
52 const auto root = ASN1::Object(rawData.begin(), rawData.end());
53 if (root.tag() != V_ASN1_SEQUENCE) {
54 qCWarning(Log) << "wrong ASN1 root node type" << root.tagName();
55 return {};
56 }
57
58 // read outer ASN1 sequence
59 IrmaProof proof;
60 auto outer = root.firstChild();
61 proof.disclosureTime = outer.readInt64();
62 outer = outer.next();
63 proof.C = outer.readBignum();
64 outer = outer.next();
65 proof.A = outer.readBignum();
66 outer = outer.next();
67 proof.EResponse = outer.readBignum();
68 outer = outer.next();
69 proof.VResponse = outer.readBignum();
70 outer = outer.next();
71 proof.AResponses.push_back(outer.readBignum());
72 outer = outer.next();
73 if (outer.tag() != V_ASN1_SEQUENCE) {
74 qCWarning(Log) << "wrong ADisclosed field type" << outer.tagName();
75 return {};
76 }
77
78 // metadata
79 // isSpecimen
80 // isPaperProof
81 // validFrom
82 // validForHours
83 // firstNameInitial
84 // lastNameInitial
85 // birthDay
86 // birthMonth
87 auto adisclosed = outer.firstChild();
88 proof.ADisclosed.push_back(adisclosed.readBignum());
89
90 // metadata: byte array containing another ASN1 sequence
91 // version: OCTET STRING
92 // issuer key id: PRINTABLESTRING
93 const auto rawMetaData = nlDecodeAsn1ByteArray(adisclosed);
94 const auto metadata = ASN1::Object(rawMetaData.begin(), rawMetaData.end());
95 if (metadata.tag() != V_ASN1_SEQUENCE) {
96 qCWarning(Log) << "meta data is not a ASN.1 SEQUENCE:" << metadata.tagName();
97 return {};
98 }
99 auto metadataEntry = metadata.firstChild();
100 const auto version = metadataEntry.readOctetString();
101 if (version.size() != 1 || version[0] != 0x02) {
102 qCWarning(Log) << "unsupported version:" << version;
103 return {};
104 }
105 metadataEntry = metadataEntry.next();
106 const auto issuer = QString::fromUtf8(metadataEntry.readPrintableString());
107
108 // isSpecimen invalidate the certificate state
109 adisclosed = adisclosed.next();
110 proof.ADisclosed.push_back(adisclosed.readBignum());
111 const auto rawIsSpecimen = nlDecodeAsn1ByteArray(adisclosed);
112 const bool isSpecimen = rawIsSpecimen.size() != 1 || rawIsSpecimen[0] != '0';
113 adisclosed = adisclosed.next();
114 proof.ADisclosed.push_back(adisclosed.readBignum());
115
116 // valid time range
117 adisclosed = adisclosed.next();
118 proof.ADisclosed.push_back(adisclosed.readBignum());
119 const auto validFrom = QDateTime::fromSecsSinceEpoch(nlDecodeAsn1ByteArray(adisclosed).toLongLong());
120 adisclosed = adisclosed.next();
121 proof.ADisclosed.push_back(adisclosed.readBignum());
122 const auto validTo = validFrom.addSecs(3600 * nlDecodeAsn1ByteArray(adisclosed).toInt());
123
124 // name
125 adisclosed = adisclosed.next();
126 proof.ADisclosed.push_back(adisclosed.readBignum());
127 auto name = QString::fromUtf8(nlDecodeAsn1ByteArray(adisclosed));
128 adisclosed = adisclosed.next();
129 proof.ADisclosed.push_back(adisclosed.readBignum());
130 name += QLatin1Char(' ') + QString::fromUtf8(nlDecodeAsn1ByteArray(adisclosed));
131
132 // birthday
133 adisclosed = adisclosed.next();
134 proof.ADisclosed.push_back(adisclosed.readBignum());
135 auto bd = QString::fromUtf8(nlDecodeAsn1ByteArray(adisclosed));
136 if (!adisclosed.hasNext()) {
137 qCWarning(Log) << "ADisclosed sequence too short";
138 return {};
139 }
140 adisclosed = adisclosed.next();
141 proof.ADisclosed.push_back(adisclosed.readBignum());
142 bd += QLatin1Char(' ') + QString::fromUtf8(nlDecodeAsn1ByteArray(adisclosed));
143 const auto birthday = QDate::fromString(bd, QStringLiteral("d M"));
144
145 // signature
146 if (proof.isNull()) {
147 return {};
148 }
149 const auto publicKey = IrmaPublicKeyLoader::load(issuer);
151 if (publicKey.isValid()) {
152 const auto sigValid = IrmaVerifier::verify(proof, publicKey);
154 }
155
156 // proof identifier (used in the revocation list)
157 const auto proofId = QCryptographicHash::hash(Bignum::toByteArray(proof.C), QCryptographicHash::Sha256).left(16).toBase64();
158
159 if (validFrom.secsTo(validTo) > 48 * 3600) {
161 cert.setCountry(QStringLiteral("NL"));
162 cert.setDisease(QStringLiteral("COVID-19"));
163 cert.setName(name);
164 cert.setDateOfBirth(birthday);
165 cert.setCertificateIssueDate(validFrom);
166 cert.setCertificateExpiryDate(validTo);
167 cert.setRawData(data);
168 cert.setSignatureState(isSpecimen ? KHealthCertificate::InvalidSignature : sigState);
169 cert.setCertificateId(QString::fromLatin1(proofId));
170 return cert;
171 } else {
172 KTestCertificate cert;
173 cert.setCountry(QStringLiteral("NL"));
174 cert.setDisease(QStringLiteral("COVID-19"));
175 cert.setResult(KTestCertificate::Negative);
176 cert.setName(name);
177 cert.setDateOfBirth(birthday);
178 cert.setCertificateIssueDate(validFrom);
179 cert.setCertificateExpiryDate(validTo);
180 cert.setRawData(data);
181 cert.setSignatureState(isSpecimen ? KHealthCertificate::InvalidSignature : sigState);
182 cert.setCertificateId(QString::fromLatin1(proofId));
183 return cert;
184 }
185}
@ 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 test certificate.
A vaccination certificate.
KDB_EXPORT KDbVersionInfo version()
QString name(StandardAction id)
iterator begin()
iterator end()
QByteArray left(qsizetype len) const const
qsizetype size() const const
bool startsWith(QByteArrayView bv) const const
QByteArray toBase64(Base64Options options) const const
QByteArray hash(QByteArrayView data, Algorithm method)
QDate fromString(QStringView string, QStringView format, QCalendar cal)
QDateTime fromSecsSinceEpoch(qint64 secs)
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 24 2024 11:51:28 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.