116 lines
5.3 KiB
C++
116 lines
5.3 KiB
C++
#include "PolicyHelper.h"
|
|
#include <QApplication>
|
|
#include <QDateTime>
|
|
#include <QFile>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QSaveFile>
|
|
#include <algorithm>
|
|
#ifdef HAVE_OPENSSL
|
|
#include <openssl/evp.h>
|
|
#include <openssl/pem.h>
|
|
#endif
|
|
|
|
PolicyHelper::PolicyHelper(const QString& baseDir) : m_baseDir(baseDir) {}
|
|
|
|
bool PolicyHelper::loadPolicy(const QString& relativePath)
|
|
{
|
|
QFile file(m_baseDir + "/" + relativePath);
|
|
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
|
|
{
|
|
QSaveFile file(m_baseDir + "/" + relativePath);
|
|
if (!file.open(QIODevice::WriteOnly)) return false;
|
|
const QByteArray bytes = QJsonDocument(m_policy).toJson(QJsonDocument::Indented);
|
|
return file.write(bytes) == bytes.size() && file.commit();
|
|
}
|
|
|
|
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<const unsigned char*>(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::isOfflineAllowed() const { return isValid() && m_policy.value("offline_allowed").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(); }
|