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 }