428 lines
14 KiB
C++
428 lines
14 KiB
C++
#include "eetca.h"
|
|
|
|
#include <QCryptographicHash>
|
|
#include <QEventLoop>
|
|
#include <QFile>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkReply>
|
|
#include <QNetworkRequest>
|
|
#include <QTimer>
|
|
|
|
#include <openssl/evp.h>
|
|
#include <openssl/pkcs12.h>
|
|
#include <openssl/x509.h>
|
|
#include <openssl/crypto.h>
|
|
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
|
#include <openssl/provider.h>
|
|
#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<unsigned char *>(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<qsizetype>(signatureSize), Qt::Uninitialized);
|
|
if (EVP_DigestSignFinal(context, reinterpret_cast<unsigned char *>(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<qsizetype>(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<QUuid> *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;
|
|
}
|