KHealthCertificate

jwsverifier.cpp
1 /*
2  * SPDX-FileCopyrightText: 2021 Volker Krause <[email protected]>
3  * SPDX-License-Identifier: LGPL-2.0-or-later
4  */
5 
6 #include "jwsverifier_p.h"
7 #include "jsonld_p.h"
8 #include "logging.h"
9 #include "rdf_p.h"
10 
11 #include <QFile>
12 #include <QJsonDocument>
13 
14 #include <openssl/err.h>
15 #include <openssl/evp.h>
16 #include <openssl/pem.h>
17 
18 JwsVerifier::JwsVerifier(const QJsonObject &doc)
19  : m_obj(doc)
20 {
21 }
22 
23 JwsVerifier::~JwsVerifier() = default;
24 
25 bool JwsVerifier::verify() const
26 {
27  const auto proof = m_obj.value(QLatin1String("proof")).toObject();
28  const auto jws = proof.value(QLatin1String("jws")).toString();
29 
30  // see RFC 7515 ยง3.1. JWS Compact Serialization Overview
31  const auto payloadStart = jws.indexOf(QLatin1Char('.'));
32  if (payloadStart < 0) {
33  return false;
34  }
35  const auto header = QStringView(jws).left(payloadStart);
36  const auto sigStart = jws.indexOf(QLatin1Char('.'), payloadStart + 1);
37  if (sigStart < 0) {
38  return false;
39  }
40  //const auto payload = QStringView(jws).mid(payloadStart + 1, sigStart - payloadStart - 1);
41  const auto signature = QByteArray::fromBase64(QStringView(jws).mid(sigStart + 1).toUtf8(), QByteArray::Base64UrlEncoding);
42 
43  // check signature algorithm
44  const auto headerObj = QJsonDocument::fromJson(QByteArray::fromBase64(header.toUtf8(), QByteArray::Base64UrlEncoding)).object();
45  if (headerObj.value(QLatin1String("alg")) != QLatin1String("PS256")) {
46  qCWarning(Log) << "not implemented JWS algorithm:" << headerObj;
47  return false;
48  }
49 
50  // load certificate
51  const auto evp = loadPublicKey();
52  if (!evp) {
53  return false;
54  }
55 
56  const EVP_MD *digest = EVP_sha256();
57  uint8_t digestData[EVP_MAX_MD_SIZE];
58  uint32_t digestSize = 0;
59 
60  // prepare the canonicalized form of the signed content
61  QJsonObject content = m_obj;
62  QJsonObject proofOptions = content.take(QLatin1String("proof")).toObject();
63  proofOptions.remove(QLatin1String("jws"));
64  proofOptions.remove(QLatin1String("signatureValue"));
65  proofOptions.remove(QLatin1String("proofValue"));
66  proofOptions.insert(QLatin1String("@context"), QLatin1String("https://w3id.org/security/v2"));
67 
68  const auto canonicalProof = canonicalRdf(proofOptions);
69  const auto canonicalContent = canonicalRdf(content);
70 
71  QByteArray signedData = header.toUtf8() + '.';
72  EVP_Digest(reinterpret_cast<const uint8_t*>(canonicalProof.constData()), canonicalProof.size(), digestData, &digestSize, digest, nullptr);
73  signedData.append(reinterpret_cast<const char*>(digestData), digestSize);
74  EVP_Digest(reinterpret_cast<const uint8_t*>(canonicalContent.constData()), canonicalContent.size(), digestData, &digestSize, digest, nullptr);
75  signedData.append(reinterpret_cast<const char*>(digestData), digestSize);
76 
77  // compute hash of the signed data
78  EVP_Digest(reinterpret_cast<const uint8_t*>(signedData.constData()), signedData.size(), digestData, &digestSize, digest, nullptr);
79 
80  // verify
81  openssl::evp_pkey_ctx_ptr ctx(EVP_PKEY_CTX_new(evp.get(), nullptr), &EVP_PKEY_CTX_free);
82  if (!ctx || EVP_PKEY_verify_init(ctx.get()) <= 0) {
83  return false;
84  }
85  if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_PSS_PADDING) <= 0 || EVP_PKEY_CTX_set_signature_md(ctx.get(), digest) <= 0) {
86  return false;
87  }
88 
89  const auto verifyResult = EVP_PKEY_verify(ctx.get(), reinterpret_cast<const uint8_t*>(signature.constData()), signature.size(), digestData, digestSize);
90  switch (verifyResult) {
91  case -1: // technical issue
92  qCWarning(Log) << "Failed to verify signature:" << ERR_error_string(ERR_get_error(), nullptr);
93  break;
94  case 1: // valid signature;
95  return true;
96  }
97  return false;
98 }
99 
100 openssl::evp_pkey_ptr JwsVerifier::loadPublicKey() const
101 {
102  // ### for now there is only one key, longer term we probably need to actually
103  // implement finding the right key here
104  openssl::evp_pkey_ptr evp(nullptr, &EVP_PKEY_free);
105 
106  QFile pemFile(QLatin1String(":/org.kde.khealthcertificate/divoc/did-india.pem"));
107  if (!pemFile.open(QFile::ReadOnly)) {
108  qCWarning(Log) << "unable to load public key file:" << pemFile.errorString();
109  return evp;
110  }
111 
112  const auto pemData = pemFile.readAll();
113  const openssl::bio_ptr bio(BIO_new_mem_buf(pemData.constData(), pemData.size()), &BIO_free_all);
114  openssl::rsa_ptr rsa(PEM_read_bio_RSA_PUBKEY(bio.get(), nullptr, nullptr, nullptr), &RSA_free);
115  if (!rsa) {
116  qCWarning(Log) << "Failed to read public key." << ERR_error_string(ERR_get_error(), nullptr);
117  return evp;
118  }
119 
120  evp.reset(EVP_PKEY_new());
121  EVP_PKEY_assign_RSA(evp.get(), rsa.release());
122  return evp;
123 }
124 
125 static struct {
126  const char *uri;
127  const char *filePath;
128 } constexpr const schema_document_table[] = {
129  { "https://www.w3.org/2018/credentials/v1", ":/org.kde.khealthcertificate/divoc/credentials-v1.json" },
130  { "https://cowin.gov.in/credentials/vaccination/v1", ":/org.kde.khealthcertificate/divoc/vaccination-v1.json" },
131  { "https://w3id.org/security/v1", ":/org.kde.khealthcertificate/divoc/security-v1.json" },
132  { "https://w3id.org/security/v2", ":/org.kde.khealthcertificate/divoc/security-v2.json" },
133 };
134 
135 QByteArray JwsVerifier::canonicalRdf(const QJsonObject &doc) const
136 {
137  JsonLd jsonLd;
138  const auto documentLoader = [](const QString &context) -> QByteArray {
139  for (const auto &i : schema_document_table) {
140  if (context == QLatin1String(i.uri)) {
141  QFile f(QLatin1String(i.filePath));
142  if (!f.open(QFile::ReadOnly)) {
143  qCWarning(Log) << f.errorString();
144  } else {
145  return f.readAll();
146  }
147  }
148  }
149  qCWarning(Log) << "Failed to provide requested document:" << context;
150  return QByteArray();
151  };
152  jsonLd.setDocumentLoader(documentLoader);
153 
154  auto quads = jsonLd.toRdf(doc);
155  Rdf::normalize(quads);
156  return Rdf::serialize(quads);
157 }
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
QJsonValue take(const QString &key)
qsizetype indexOf(QChar c, qsizetype from, Qt::CaseSensitivity cs) const const
QJsonObject toObject() const const
const char * constData() const const
QByteArray & append(char ch)
QByteArray fromBase64(const QByteArray &base64, QByteArray::Base64Options options)
void remove(const QString &key)
int size() const const
QJsonObject::iterator insert(const QString &key, const QJsonValue &value)
QStringView left(qsizetype length) const const
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.