#include "eetca.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if OPENSSL_VERSION_NUMBER >= 0x30000000L #include #endif namespace { struct SigningMaterial { EVP_PKEY *key{nullptr}; QByteArray certificateDer; }; void setError(QString *error, const QString &message) { if (error != nullptr) { *error = message; } } QByteArray base64Url(const QByteArray &data) { return data.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); } bool loadSigningMaterial(const QString &path, const QByteArray &password, SigningMaterial *material, QString *error) { OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CONFIG, nullptr); #if OPENSSL_VERSION_NUMBER >= 0x30000000L OSSL_PROVIDER_load(nullptr, "default"); OSSL_PROVIDER_load(nullptr, "legacy"); #endif QFile file(path); if (!file.open(QIODevice::ReadOnly)) { setError(error, QStringLiteral("Cannot read the CA signing PKCS#12 file.")); return false; } const QByteArray data = file.readAll(); BIO *bio = BIO_new_mem_buf(data.constData(), data.size()); PKCS12 *container = bio == nullptr ? nullptr : d2i_PKCS12_bio(bio, nullptr); BIO_free(bio); if (container == nullptr) { setError(error, QStringLiteral("The CA signing PKCS#12 file is invalid.")); return false; } EVP_PKEY *key = nullptr; X509 *certificate = nullptr; STACK_OF(X509) *chain = nullptr; const bool parsed = PKCS12_parse(container, password.constData(), &key, &certificate, &chain) == 1; PKCS12_free(container); sk_X509_pop_free(chain, X509_free); if (!parsed || key == nullptr || certificate == nullptr) { EVP_PKEY_free(key); X509_free(certificate); setError(error, QStringLiteral("The CA signing PKCS#12 password is invalid.")); return false; } const int length = i2d_X509(certificate, nullptr); if (length <= 0) { EVP_PKEY_free(key); X509_free(certificate); setError(error, QStringLiteral("The CA signing certificate is invalid.")); return false; } material->certificateDer.resize(length); unsigned char *destination = reinterpret_cast(material->certificateDer.data()); if (i2d_X509(certificate, &destination) != length) { EVP_PKEY_free(key); X509_free(certificate); material->certificateDer.clear(); setError(error, QStringLiteral("The CA signing certificate cannot be encoded.")); return false; } material->key = key; X509_free(certificate); return true; } QByteArray createJwt(const SigningMaterial &material, QString *error) { const QByteArray thumbprint = base64Url( QCryptographicHash::hash(material.certificateDer, QCryptographicHash::Sha256)); const QByteArray header = QJsonDocument(QJsonObject{ {QStringLiteral("alg"), QStringLiteral("RS256")}, {QStringLiteral("typ"), QStringLiteral("JWT")}, {QStringLiteral("x5t#S256"), QString::fromLatin1(thumbprint)} }).toJson(QJsonDocument::Compact); const QByteArray payload = QByteArrayLiteral("{}"); const QByteArray signingInput = base64Url(header) + '.' + base64Url(payload); EVP_MD_CTX *context = EVP_MD_CTX_new(); if (context == nullptr || EVP_DigestSignInit(context, nullptr, EVP_sha256(), nullptr, material.key) != 1 || EVP_DigestSignUpdate(context, signingInput.constData(), signingInput.size()) != 1) { EVP_MD_CTX_free(context); setError(error, QStringLiteral("Cannot initialize CA JWT signing.")); return {}; } size_t signatureSize = 0; if (EVP_DigestSignFinal(context, nullptr, &signatureSize) != 1) { EVP_MD_CTX_free(context); setError(error, QStringLiteral("Cannot determine CA JWT signature size.")); return {}; } QByteArray signature(static_cast(signatureSize), Qt::Uninitialized); if (EVP_DigestSignFinal(context, reinterpret_cast(signature.data()), &signatureSize) != 1) { EVP_MD_CTX_free(context); setError(error, QStringLiteral("Cannot create CA JWT signature.")); return {}; } EVP_MD_CTX_free(context); signature.resize(static_cast(signatureSize)); return signingInput + '.' + base64Url(signature); } bool parseJsonObject(const QByteArray &data, QJsonObject *object, QString *error) { QJsonParseError parseError; const QJsonDocument document = QJsonDocument::fromJson(data, &parseError); if (parseError.error != QJsonParseError::NoError || !document.isObject()) { setError(error, QStringLiteral("CA API returned invalid JSON.")); return false; } *object = document.object(); return true; } bool parseApiError(const QByteArray &data, int statusCode, QString *error) { QJsonObject object; if (parseJsonObject(data, &object, nullptr)) { const QString message = object.value(QStringLiteral("message")).toString(); if (!message.isEmpty()) { setError(error, QStringLiteral("CA API HTTP %1: %2").arg(statusCode).arg(message)); return false; } } setError(error, QStringLiteral("CA API returned HTTP status %1.").arg(statusCode)); return false; } bool parseCertificate(const QJsonObject &object, EetCaCertificate *certificate) { certificate->serialNumber = object.value(QStringLiteral("sn")).toString(); certificate->validFrom = QDateTime::fromString( object.value(QStringLiteral("validFrom")).toString(), Qt::ISODate); certificate->validTo = QDateTime::fromString( object.value(QStringLiteral("validTo")).toString(), Qt::ISODate); certificate->state = object.value(QStringLiteral("state")).toString(); certificate->description = object.value(QStringLiteral("description")).toString(); certificate->email = object.value(QStringLiteral("email")).toString(); return !certificate->serialNumber.isEmpty() && certificate->validFrom.isValid() && certificate->validTo.isValid() && !certificate->state.isEmpty(); } bool performRequest(const EetCaClientConfig &config, const QString &path, const QByteArray &method, QByteArray *data, int *statusCode, QString *error) { if (!config.apiBaseUrl.isValid() || config.apiBaseUrl.scheme() != QLatin1String("https") || config.apiBaseUrl.host().isEmpty() || config.timeoutMilliseconds <= 0) { setError(error, QStringLiteral("CA API requires a valid HTTPS base URL and positive timeout.")); return false; } SigningMaterial material; if (!loadSigningMaterial(config.signingPkcs12Path, config.signingPkcs12Password, &material, error)) { return false; } const QByteArray jwt = createJwt(material, error); EVP_PKEY_free(material.key); if (jwt.isEmpty()) { return false; } QNetworkAccessManager manager; QUrl endpoint(config.apiBaseUrl); QString basePath = endpoint.path(); if (!basePath.endsWith(QLatin1Char('/'))) { basePath.append(QLatin1Char('/')); } endpoint.setPath(basePath + path.mid(1)); QNetworkRequest request(endpoint); if (!config.sslConfiguration.isNull()) { request.setSslConfiguration(config.sslConfiguration); } request.setRawHeader("Authorization", "Bearer " + jwt); QNetworkReply *reply = method == QByteArrayLiteral("POST") ? manager.post(request, QByteArray()) : manager.get(request); QEventLoop loop; QTimer timer; timer.setSingleShot(true); bool timedOut = false; QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); QObject::connect(&timer, &QTimer::timeout, reply, [&] { timedOut = true; reply->abort(); }); timer.start(config.timeoutMilliseconds); loop.exec(); if (timedOut || reply->error() != QNetworkReply::NoError) { setError(error, timedOut ? QStringLiteral("CA API request timed out.") : reply->errorString()); reply->deleteLater(); return false; } *statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); *data = reply->readAll(); reply->deleteLater(); return true; } } EetCaClient::EetCaClient(EetCaClientConfig config) : m_config(std::move(config)) { } bool EetCaClient::requestRenewal(QUuid *requestId, QString *error) const { if (requestId == nullptr) { setError(error, QStringLiteral("A request ID output object is required.")); return false; } *requestId = {}; QByteArray data; int statusCode = 0; if (!performRequest(m_config, QStringLiteral("/request/renew"), QByteArrayLiteral("POST"), &data, &statusCode, error)) { return false; } if (statusCode != 201) { return parseApiError(data, statusCode, error); } QJsonObject object; if (!parseJsonObject(data, &object, error)) { return false; } const QUuid id(object.value(QStringLiteral("reqId")).toString()); if (id.isNull()) { setError(error, QStringLiteral("CA API returned an invalid request ID.")); return false; } *requestId = id; return true; } bool EetCaClient::requestStatus(const QUuid &requestId, EetCaRequestStatus *status, QString *error) const { if (status == nullptr || requestId.isNull()) { setError(error, QStringLiteral("A valid request ID and output object are required.")); return false; } QByteArray data; int statusCode = 0; if (!performRequest(m_config, QStringLiteral("/request/%1/status").arg( requestId.toString(QUuid::WithoutBraces)), QByteArrayLiteral("GET"), &data, &statusCode, error)) { return false; } if (statusCode != 200) { return parseApiError(data, statusCode, error); } QJsonObject object; if (!parseJsonObject(data, &object, error)) { return false; } const QUuid returnedId(object.value(QStringLiteral("reqId")).toString()); const QString state = object.value(QStringLiteral("state")).toString(); if (returnedId.isNull() || state.isEmpty()) { setError(error, QStringLiteral("CA API returned an invalid request status.")); return false; } status->requestId = returnedId; status->state = state; status->pollAfterSeconds = object.value(QStringLiteral("pollAfterSeconds")).toInt(); status->rejectedReason = object.value(QStringLiteral("rejectedReason")).toString(); return true; } bool EetCaClient::listUnfinished(QList *requestIds, QString *error) const { if (requestIds == nullptr) { setError(error, QStringLiteral("A request ID list output object is required.")); return false; } requestIds->clear(); QByteArray data; int statusCode = 0; if (!performRequest(m_config, QStringLiteral("/request/not-finished"), QByteArrayLiteral("GET"), &data, &statusCode, error)) { return false; } if (statusCode != 200) { return parseApiError(data, statusCode, error); } QJsonObject object; if (!parseJsonObject(data, &object, error)) { return false; } for (const QJsonValue &value : object.value(QStringLiteral("data")).toArray()) { const QUuid id(value.toString()); if (id.isNull()) { setError(error, QStringLiteral("CA API returned an invalid request ID.")); requestIds->clear(); return false; } requestIds->append(id); } return true; } bool EetCaClient::claimDownload(const QUuid &requestId, EetCaPkcs12 *result, QString *error) const { if (result == nullptr || requestId.isNull()) { setError(error, QStringLiteral("A valid request ID and output object are required.")); return false; } *result = {}; QByteArray data; int statusCode = 0; if (!performRequest(m_config, QStringLiteral("/request/%1/claim-download").arg( requestId.toString(QUuid::WithoutBraces)), QByteArrayLiteral("POST"), &data, &statusCode, error)) { return false; } if (statusCode != 200) { return parseApiError(data, statusCode, error); } QJsonObject object; if (!parseJsonObject(data, &object, error)) { return false; } result->pkcs12 = QByteArray::fromBase64( object.value(QStringLiteral("pkcs12")).toString().toLatin1()); result->password = object.value(QStringLiteral("password")).toString().toUtf8(); if (result->pkcs12.isEmpty() || result->password.isEmpty() || !parseCertificate(object.value(QStringLiteral("certificate")).toObject(), &result->certificate)) { *result = {}; setError(error, QStringLiteral("CA API returned an invalid certificate package.")); return false; } return true; } bool EetCaClient::acknowledgeDownload(const QUuid &requestId, QString *error) const { if (requestId.isNull()) { setError(error, QStringLiteral("A valid request ID is required.")); return false; } QByteArray data; int statusCode = 0; if (!performRequest(m_config, QStringLiteral("/request/%1/ack-download").arg( requestId.toString(QUuid::WithoutBraces)), QByteArrayLiteral("POST"), &data, &statusCode, error)) { return false; } if (statusCode != 204) { return parseApiError(data, statusCode, error); } return true; }