File indexing completed on 2024-11-24 04:44:39

0001 /*
0002  * SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
0003  * SPDX-License-Identifier: LGPL-2.0-or-later
0004  */
0005 
0006 #include "jwsverifier_p.h"
0007 #include "jsonld_p.h"
0008 #include "logging.h"
0009 #include "rdf_p.h"
0010 
0011 #include <QFile>
0012 #include <QJsonDocument>
0013 
0014 #include <openssl/err.h>
0015 #include <openssl/evp.h>
0016 #include <openssl/pem.h>
0017 
0018 JwsVerifier::JwsVerifier(const QJsonObject &doc)
0019     : m_obj(doc)
0020 {
0021 }
0022 
0023 JwsVerifier::~JwsVerifier() = default;
0024 
0025 bool JwsVerifier::verify() const
0026 {
0027     const auto proof = m_obj.value(QLatin1String("proof")).toObject();
0028     const auto jws = proof.value(QLatin1String("jws")).toString();
0029 
0030     // see RFC 7515 ยง3.1. JWS Compact Serialization Overview
0031     const auto payloadStart = jws.indexOf(QLatin1Char('.'));
0032     if (payloadStart < 0) {
0033         return false;
0034     }
0035     const auto header = QStringView(jws).left(payloadStart);
0036     const auto sigStart = jws.indexOf(QLatin1Char('.'), payloadStart + 1);
0037     if (sigStart < 0) {
0038         return false;
0039     }
0040     //const auto payload = QStringView(jws).mid(payloadStart + 1, sigStart - payloadStart - 1);
0041     const auto signature = QByteArray::fromBase64(QStringView(jws).mid(sigStart + 1).toUtf8(), QByteArray::Base64UrlEncoding);
0042 
0043     // check signature algorithm
0044     const auto headerObj = QJsonDocument::fromJson(QByteArray::fromBase64(header.toUtf8(), QByteArray::Base64UrlEncoding)).object();
0045     if (headerObj.value(QLatin1String("alg")) != QLatin1String("PS256")) {
0046         qCWarning(Log) << "not implemented JWS algorithm:" << headerObj;
0047         return false;
0048     }
0049 
0050     // load certificate
0051     const auto evp = loadPublicKey();
0052     if (!evp) {
0053         return false;
0054     }
0055 
0056     const EVP_MD *digest = EVP_sha256();
0057     uint8_t digestData[EVP_MAX_MD_SIZE];
0058     uint32_t  digestSize = 0;
0059 
0060     // prepare the canonicalized form of the signed content
0061     QJsonObject content = m_obj;
0062     QJsonObject proofOptions = content.take(QLatin1String("proof")).toObject();
0063     proofOptions.remove(QLatin1String("jws"));
0064     proofOptions.remove(QLatin1String("signatureValue"));
0065     proofOptions.remove(QLatin1String("proofValue"));
0066     proofOptions.insert(QLatin1String("@context"), QLatin1String("https://w3id.org/security/v2"));
0067 
0068     const auto canonicalProof = canonicalRdf(proofOptions);
0069     const auto canonicalContent = canonicalRdf(content);
0070 
0071     QByteArray signedData = header.toUtf8() + '.';
0072     EVP_Digest(reinterpret_cast<const uint8_t*>(canonicalProof.constData()), canonicalProof.size(), digestData, &digestSize, digest, nullptr);
0073     signedData.append(reinterpret_cast<const char*>(digestData), digestSize);
0074     EVP_Digest(reinterpret_cast<const uint8_t*>(canonicalContent.constData()), canonicalContent.size(), digestData, &digestSize, digest, nullptr);
0075     signedData.append(reinterpret_cast<const char*>(digestData), digestSize);
0076 
0077     // compute hash of the signed data
0078     EVP_Digest(reinterpret_cast<const uint8_t*>(signedData.constData()), signedData.size(), digestData, &digestSize, digest, nullptr);
0079 
0080     // verify
0081     openssl::evp_pkey_ctx_ptr ctx(EVP_PKEY_CTX_new(evp.get(), nullptr));
0082     if (!ctx || EVP_PKEY_verify_init(ctx.get()) <= 0) {
0083         return false;
0084     }
0085     if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_PSS_PADDING) <= 0 || EVP_PKEY_CTX_set_signature_md(ctx.get(), digest) <= 0) {
0086         return false;
0087     }
0088 
0089     const auto verifyResult = EVP_PKEY_verify(ctx.get(), reinterpret_cast<const uint8_t*>(signature.constData()), signature.size(),  digestData, digestSize);
0090     switch (verifyResult) {
0091         case -1: // technical issue
0092             qCWarning(Log) << "Failed to verify signature:" << ERR_error_string(ERR_get_error(), nullptr);
0093             break;
0094         case 1: // valid signature;
0095             return true;
0096     }
0097     return false;
0098 }
0099 
0100 openssl::evp_pkey_ptr JwsVerifier::loadPublicKey() const
0101 {
0102     // ### for now there is only one key, longer term we probably need to actually
0103     // implement finding the right key here
0104     QFile pemFile(QLatin1String(":/org.kde.khealthcertificate/divoc/did-india.pem"));
0105     if (!pemFile.open(QFile::ReadOnly)) {
0106         qCWarning(Log) << "unable to load public key file:" << pemFile.errorString();
0107         return {};
0108     }
0109 
0110     const auto pemData = pemFile.readAll();
0111     const openssl::bio_ptr bio(BIO_new_mem_buf(pemData.constData(), pemData.size()));
0112     openssl::rsa_ptr rsa(PEM_read_bio_RSA_PUBKEY(bio.get(), nullptr, nullptr, nullptr));
0113     if (!rsa) {
0114         qCWarning(Log) << "Failed to read public key." << ERR_error_string(ERR_get_error(), nullptr);
0115         return {};
0116     }
0117 
0118     openssl::evp_pkey_ptr evp(EVP_PKEY_new());
0119     EVP_PKEY_assign_RSA(evp.get(), rsa.release());
0120     return evp;
0121 }
0122 
0123 static struct {
0124     const char *uri;
0125     const char *filePath;
0126 } constexpr const schema_document_table[] = {
0127     { "https://www.w3.org/2018/credentials/v1", ":/org.kde.khealthcertificate/divoc/credentials-v1.json" },
0128     { "https://cowin.gov.in/credentials/vaccination/v1", ":/org.kde.khealthcertificate/divoc/vaccination-v1.json" },
0129     { "https://w3id.org/security/v1", ":/org.kde.khealthcertificate/divoc/security-v1.json" },
0130     { "https://w3id.org/security/v2", ":/org.kde.khealthcertificate/divoc/security-v2.json" },
0131 };
0132 
0133 QByteArray JwsVerifier::canonicalRdf(const QJsonObject &doc) const
0134 {
0135     JsonLd jsonLd;
0136     const auto documentLoader = [](const QString &context) -> QByteArray {
0137         for (const auto &i : schema_document_table) {
0138             if (context == QLatin1String(i.uri)) {
0139                 QFile f(QLatin1String(i.filePath));
0140                 if (!f.open(QFile::ReadOnly)) {
0141                     qCWarning(Log) << f.errorString();
0142                 } else {
0143                     return f.readAll();
0144                 }
0145             }
0146         }
0147         qCWarning(Log) << "Failed to provide requested document:" << context;
0148         return QByteArray();
0149     };
0150     jsonLd.setDocumentLoader(documentLoader);
0151 
0152     auto quads = jsonLd.toRdf(doc);
0153     Rdf::normalize(quads);
0154     return Rdf::serialize(quads);
0155 }