#include "PolicyHelper.h" #include "ConfigHelper.h" #include #include #include #include #include #include #include #ifdef HAVE_OPENSSL #include #include #endif PolicyHelper::PolicyHelper(const QString& baseDir) : m_baseDir(baseDir) {} static QString resolvePolicyPath(const QString& baseDir, const QString& relativePath, bool forWrite) { const QString defaultPolicy = QStringLiteral("config/version_policy.dat"); if (relativePath == defaultPolicy) { const QString runtimePolicy = ConfigHelper::instance().policyPath(); if (forWrite || QFile::exists(runtimePolicy)) return runtimePolicy; } if (QDir::isAbsolutePath(relativePath)) return relativePath; return baseDir + "/" + relativePath; } bool PolicyHelper::loadPolicy(const QString& relativePath) { QFile file(resolvePolicyPath(m_baseDir, relativePath, false)); if (!file.open(QIODevice::ReadOnly)) { m_error = "Cannot open policy file"; return false; } QJsonParseError error; const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); if (error.error != QJsonParseError::NoError || !doc.isObject()) { m_error = "Invalid policy JSON: " + error.errorString(); return false; } return loadPolicyObject(doc.object()); } bool PolicyHelper::loadPolicyObject(const QJsonObject& policy, const QString& signedText) { m_policy = policy; m_signedText = signedText; m_loaded = true; m_error.clear(); return isValid(); } bool PolicyHelper::savePolicy(const QString& relativePath) const { const QByteArray bytes = QJsonDocument(m_policy).toJson(QJsonDocument::Indented); QString writeError; const QString path = resolvePolicyPath(m_baseDir, relativePath, true); if (!ConfigHelper::writeFileWithElevationIfNeeded(path, bytes, &writeError)) { m_error = "Cannot save policy file: " + writeError; return false; } m_error.clear(); return true; } QByteArray PolicyHelper::canonicalPolicyBytes(const QJsonObject& policy) const { QJsonObject copy = policy; copy.remove("signature"); auto sortObject = [&](auto&& self, const QJsonObject& obj) -> QJsonObject { QStringList keys = obj.keys(); std::sort(keys.begin(), keys.end()); QJsonObject sorted; for (const QString& key : keys) { QJsonValue value = obj.value(key); if (value.isObject()) value = self(self, value.toObject()); else if (value.isArray()) { QJsonArray array; for (QJsonValue item : value.toArray()) { if (item.isObject()) item = self(self, item.toObject()); array.append(item); } value = array; } sorted.insert(key, value); } return sorted; }; return QJsonDocument(sortObject(sortObject, copy)).toJson(QJsonDocument::Compact); } bool PolicyHelper::verifySignature(const QByteArray& payload, const QString& signatureBase64) const { #ifndef HAVE_OPENSSL Q_UNUSED(payload); Q_UNUSED(signatureBase64); m_error = "OpenSSL unavailable"; return false; #else QString keyPath = m_baseDir + "/config/manifest_public_key.pem"; QFile keyFile(keyPath); if (!keyFile.open(QIODevice::ReadOnly)) { m_error = "Cannot open policy public key"; return false; } const QByteArray keyData = keyFile.readAll(); BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size()); EVP_PKEY* key = bio ? PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr) : nullptr; if (bio) BIO_free(bio); if (!key) { m_error = "Invalid policy public key"; return false; } EVP_MD_CTX* ctx = EVP_MD_CTX_new(); const QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8()); bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) == 1 && EVP_DigestVerifyUpdate(ctx, payload.constData(), payload.size()) == 1 && EVP_DigestVerifyFinal(ctx, reinterpret_cast(signature.constData()), signature.size()) == 1; if (ctx) EVP_MD_CTX_free(ctx); EVP_PKEY_free(key); if (!ok) m_error = "Invalid RSA policy signature"; return ok; #endif } bool PolicyHelper::isValid() const { if (!m_loaded) return false; const QStringList required{"app_id","channel","current_version","policy_seq","allow_run", "force_update","allow_rollback","offline_allowed","valid_until","signature_alg","key_id","signature"}; for (const QString& key : required) if (!m_policy.contains(key)) { m_error = "Missing policy field: " + key; return false; } if (m_policy.value("signature_alg").toString() != "RSA-2048-SHA256") return false; const QByteArray payload = m_signedText.isEmpty() ? canonicalPolicyBytes(m_policy) : m_signedText.toUtf8(); return verifySignature(payload, m_policy.value("signature").toString()); } QString PolicyHelper::errorString() const { return m_error; } bool PolicyHelper::isVersionAllowed(const QString& version) const { if (!isValid()) return false; for (const QJsonValue& v : m_policy.value("disabled_versions").toArray()) if (v.toString() == version) return false; return true; } bool PolicyHelper::allowRun() const { return isValid() && m_policy.value("allow_run").toBool(); } bool PolicyHelper::forceUpdate() const { return isValid() && m_policy.value("force_update").toBool(); } bool PolicyHelper::allowRollback() const { return isValid() && m_policy.value("allow_rollback").toBool(); } bool PolicyHelper::isOfflineAllowed() const { return isValid() && m_policy.value("offline_allowed").toBool(); } bool PolicyHelper::gitTagsEnabled() const { return isValid() && m_policy.value("git_tags_enabled").toBool(); } bool PolicyHelper::isExpired() const { if (!isValid()) return true; const QDateTime expiry = QDateTime::fromString(m_policy.value("valid_until").toString(), Qt::ISODate); return !expiry.isValid() || QDateTime::currentDateTimeUtc() > expiry; } qint64 PolicyHelper::policySeq() const { return isValid() ? m_policy.value("policy_seq").toVariant().toLongLong() : 0; } QString PolicyHelper::message() const { return m_policy.value("message").toString(); }