KHealthCertificate

jwsverifier.cpp
1/*
2 * SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
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
18JwsVerifier::JwsVerifier(const QJsonObject &doc)
19 : m_obj(doc)
20{
21}
22
23JwsVerifier::~JwsVerifier() = default;
24
25bool 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
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));
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
100openssl::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 QFile pemFile(QLatin1String(":/org.kde.khealthcertificate/divoc/did-india.pem"));
105 if (!pemFile.open(QFile::ReadOnly)) {
106 qCWarning(Log) << "unable to load public key file:" << pemFile.errorString();
107 return {};
108 }
109
110 const auto pemData = pemFile.readAll();
111 const openssl::bio_ptr bio(BIO_new_mem_buf(pemData.constData(), pemData.size()));
112 openssl::rsa_ptr rsa(PEM_read_bio_RSA_PUBKEY(bio.get(), nullptr, nullptr, nullptr));
113 if (!rsa) {
114 qCWarning(Log) << "Failed to read public key." << ERR_error_string(ERR_get_error(), nullptr);
115 return {};
116 }
117
118 openssl::evp_pkey_ptr evp(EVP_PKEY_new());
119 EVP_PKEY_assign_RSA(evp.get(), rsa.release());
120 return evp;
121}
122
123static struct {
124 const char *uri;
125 const char *filePath;
126} constexpr const schema_document_table[] = {
127 { "https://www.w3.org/2018/credentials/v1", ":/org.kde.khealthcertificate/divoc/credentials-v1.json" },
128 { "https://cowin.gov.in/credentials/vaccination/v1", ":/org.kde.khealthcertificate/divoc/vaccination-v1.json" },
129 { "https://w3id.org/security/v1", ":/org.kde.khealthcertificate/divoc/security-v1.json" },
130 { "https://w3id.org/security/v2", ":/org.kde.khealthcertificate/divoc/security-v2.json" },
131};
132
133QByteArray JwsVerifier::canonicalRdf(const QJsonObject &doc) const
134{
135 JsonLd jsonLd;
136 const auto documentLoader = [](const QString &context) -> QByteArray {
137 for (const auto &i : schema_document_table) {
138 if (context == QLatin1String(i.uri)) {
139 QFile f(QLatin1String(i.filePath));
140 if (!f.open(QFile::ReadOnly)) {
141 qCWarning(Log) << f.errorString();
142 } else {
143 return f.readAll();
144 }
145 }
146 }
147 qCWarning(Log) << "Failed to provide requested document:" << context;
148 return QByteArray();
149 };
150 jsonLd.setDocumentLoader(documentLoader);
151
152 auto quads = jsonLd.toRdf(doc);
153 Rdf::normalize(quads);
154 return Rdf::serialize(quads);
155}
QByteArray & append(QByteArrayView data)
const char * constData() const const
QByteArray fromBase64(const QByteArray &base64, Base64Options options)
qsizetype size() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
iterator insert(QLatin1StringView key, const QJsonValue &value)
void remove(QLatin1StringView key)
QJsonValue take(QLatin1StringView key)
QJsonObject toObject() const const
QStringView left(qsizetype length) const const
qsizetype indexOf(QChar c, qsizetype from, Qt::CaseSensitivity cs) const const
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.