/*
 * Copyright (C) 2014-2026 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <algorithm> /* ::std::sort */
#include <cstdlib> /* ::std::free */

#include "src/datovka_shared/crypto/crypto_funcs.h"
#include "src/datovka_shared/crypto/crypto_wrapped.h"
#include "src/datovka_shared/log/log.h"

TstEntries::TstEntries(void)
    : m_timeStampList(NULL),
    m_envelope_tst(NULL),
    m_envelope_tst_valid(false),
    m_envelope_tst_time(-1),
    m_signature_tst(NULL),
    m_signature_tst_valid(false),
    m_signature_tst_time(-1),
    m_archiveTsts(),
    m_sortedArchiveTstTimes()
{
}

TstEntries::~TstEntries(void)
{
	der_data_list_deep_free(m_timeStampList);
}

bool TstEntries::addTst(enum der_data_type type, const QByteArray &tstDER)
{
	struct der_data_list_entry *new_entry = der_data_list_new(type,
	    tstDER.data(), tstDER.size());
	if (Q_UNLIKELY(NULL == new_entry)) {
		return false;
	}

	if (NULL == m_timeStampList) {
		m_timeStampList = new_entry; new_entry = NULL;
	} else {
		struct der_data_list_entry *last =
		    der_data_list_last(m_timeStampList);
		if (Q_UNLIKELY(NULL == last)) {
			Q_ASSERT(0);
			goto fail;
		}
		last->next = new_entry; new_entry = NULL;
	}

	return true;

fail:
	der_data_list_deep_free(new_entry);
	return false;
}

bool TstEntries::addCmsTsts(const QByteArray &msgDER)
{
	return (0 == raw_cms_timestamps(msgDER.data(), msgDER.size(),
	    &m_timeStampList));
}

void TstEntries::sortTimestamps(void)
{
	m_envelope_tst = NULL;
	m_envelope_tst_valid = false;
	m_envelope_tst_time = -1;
	m_signature_tst = NULL;
	m_signature_tst_valid = false;
	m_signature_tst_time = -1;
	m_archiveTsts.clear();
	m_sortedArchiveTstTimes.clear();

	{
		const struct der_data_list_entry *entry = m_timeStampList;
		while (NULL != entry) {
			int64_t utc_time = 0;
			int ret = 0;
			switch (entry->der.type) {
			case DER_XML_TST:
				if (NULL == m_envelope_tst) {
					m_envelope_tst = entry;
					ret = raw_tst_verify(entry->der.data, entry->der.size, &utc_time);
					if (-1 != ret) {
						m_envelope_tst_valid = (1 == ret);
						m_envelope_tst_time = utc_time;
					} else {
						logErrorNL("%s", "Cannot read envelope timestamp time.");
					}
				} else {
					logErrorNL("%s", "Envelope timestamp already present.");
				}
				break;
			case DER_TST:
				if (NULL == m_signature_tst) {
					m_signature_tst = entry;
					ret = raw_tst_verify(entry->der.data, entry->der.size, &utc_time);
					if (-1 != ret) {
						m_signature_tst_valid = (1 == ret);
						m_signature_tst_time = utc_time;
					} else {
						logErrorNL("%s", "Cannot read signature timestamp time.");
					}
				} else {
					logErrorNL("%s", "Signature timestamp already present.");
				}
				break;
			case DER_ARCHIVE_TST:
				ret = raw_tst_verify(entry->der.data, entry->der.size, &utc_time);
				if (-1 != ret) {
					m_archiveTsts[utc_time] =
					    QPair<const struct der_data_list_entry *, bool>(entry, (1 == ret));
				} else {
					logErrorNL("%s", "Cannot read archive timestamp time.");
				}
				break;
			default:
				break;
			}

			entry = entry->next;
		}

		m_sortedArchiveTstTimes = m_archiveTsts.keys();
		::std::sort(::std::begin(m_sortedArchiveTstTimes), ::std::end(m_sortedArchiveTstTimes));
	}
}

const struct der_data_list_entry *TstEntries::envelopeTst(void) const
{
	return m_envelope_tst;
}

bool TstEntries::envelopeTstValid(void) const
{
	return m_envelope_tst_valid;
}

qint64 TstEntries::envelopeTstTime(void) const
{
	return m_envelope_tst_time;
}

const struct der_data_list_entry *TstEntries::signatureTst(void) const
{
	return m_signature_tst;
}

bool TstEntries::signatureTstValid(void) const
{
	return m_signature_tst_valid;
}

qint64 TstEntries::signatureTstTime(void) const
{
	return m_signature_tst_time;
}

bool TstEntries::archiveTstValid(qint64 time) const
{
	return m_archiveTsts.value(time,
	    QPair<const struct der_data_list_entry *, bool>(NULL, false)).second;
}

const struct der_data_list_entry *TstEntries::archiveTst(qint64 time) const
{
	return m_archiveTsts.value(time,
	    QPair<const struct der_data_list_entry *, bool>(NULL, false)).first;
}

const QList<qint64> &TstEntries::sortedArchiveTstTimes(void) const
{
	return m_sortedArchiveTstTimes;
}

enum TstEntries::Validity TstEntries::latestTstValid(const QByteArray &msgDER,
    QDateTime &tstTime)
{
	/*
	 * Ignore timestamp in message envelope as it is not related
	 * to the CMS container.
	 */

	TstEntries tstEntries;

	if (Q_UNLIKELY(!tstEntries.addCmsTsts(msgDER))) {
		tstTime = QDateTime();
		return MISSING;
	}

	tstEntries.sortTimestamps();

	qint64 latestTstTime = tstEntries.signatureTstTime();
	const QList<qint64> &sortedArchiveTstTimes = tstEntries.sortedArchiveTstTimes();

	if (sortedArchiveTstTimes.isEmpty()) {
		if (latestTstTime >= 0) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
			tstTime = QDateTime::fromSecsSinceEpoch(latestTstTime);
#else /* < Qt-5.8 */
			tstTime = QDateTime::fromMSecsSinceEpoch(latestTstTime * 1000);
#endif /* >= Qt-5.8 */
			return tstEntries.signatureTstValid() ? VALID : INVALID;
		}

		/* Fail if proceeds here. */
		tstTime = QDateTime();
		return MISSING;
	}

	/* Have archive timestamps. */
	latestTstTime = sortedArchiveTstTimes.last();

	if (latestTstTime >= 0) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
		tstTime = QDateTime::fromSecsSinceEpoch(latestTstTime);
#else /* < Qt-5.8 */
		tstTime = QDateTime::fromMSecsSinceEpoch(latestTstTime * 1000);
#endif /* >= Qt-5.8 */
		return tstEntries.archiveTstValid(latestTstTime) ? VALID : INVALID;
	}

	/* Fail if proceeds here. */
	tstTime = QDateTime();
	return MISSING;
}

/*!
 * @brief Returns signing certificate inception and expiration date.
 *
 * @param[in] der Buffer containing DER encoded CMS or timestamp data.
 * @param[in] der_size DER size.
 * @param[out] incTime Inception time.
 * @param[out] expTime Expiration time.
 * @return True on success.
 */
static inline
bool _signingCertTimes(const void *der, size_t der_size, QDateTime &incTime,
    QDateTime &expTime)
{
	struct x509_crt *x509_crt = NULL;
	int64_t incept, expir;

	debugFuncCall();

	x509_crt = raw_cms_signing_cert(der, der_size);
	if (Q_UNLIKELY(NULL == x509_crt)) {
		return false;
	}

	if (Q_UNLIKELY(0 != x509_crt_date_info(x509_crt, &incept, &expir))) {
		x509_crt_destroy(x509_crt); x509_crt = NULL;
		return false;
	}

	x509_crt_destroy(x509_crt); x509_crt = NULL;

#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
	incTime = QDateTime::fromSecsSinceEpoch(incept);
	expTime = QDateTime::fromSecsSinceEpoch(expir);
#else /* < Qt-5.8 */
	incTime = QDateTime::fromMSecsSinceEpoch(incept * 1000);
	expTime = QDateTime::fromMSecsSinceEpoch(expir * 1000);
#endif /* >= Qt-5.8 */

	return true;
}

bool TstEntries::latestTstSigningCertTimes(const QByteArray &msgDER,
    QDateTime &incTime, QDateTime &expTime)
{
	/*
	 * Ignore timestamp in message envelope as it is not related
	 * to the CMS container.
	 */

	TstEntries tstEntries;

	if (Q_UNLIKELY(!tstEntries.addCmsTsts(msgDER))) {
		incTime = QDateTime();
		expTime = QDateTime();
		return false;
	}

	tstEntries.sortTimestamps();

	qint64 latestTstTime = tstEntries.signatureTstTime();
	const QList<qint64> &sortedArchiveTstTimes = tstEntries.sortedArchiveTstTimes();

	if (sortedArchiveTstTimes.isEmpty()) {
		const struct der_data_list_entry *entry = tstEntries.signatureTst();
		if ((latestTstTime >= 0) && (NULL != entry)) {
			return _signingCertTimes(entry->der.data, entry->der.size,
			    incTime, expTime);
		}

		/* Fail if proceeds here. */
		incTime = QDateTime();
		expTime = QDateTime();
		return false;
	}

	/* Have archive timestamps. */
	latestTstTime = sortedArchiveTstTimes.last();

	const struct der_data_list_entry *entry = tstEntries.archiveTst(latestTstTime);
	if ((latestTstTime >= 0) && (NULL != entry)) {
		return _signingCertTimes(entry->der.data, entry->der.size,
		    incTime, expTime);
	}

	/* Fail if proceeds here. */
	incTime = QDateTime();
	expTime = QDateTime();
	return false;
}

bool TstEntries::latestTstSigningCertExpiresBefore(const QByteArray &msgDER,
    int days, const QDateTime &dDate)
{
	QDateTime expir;
	QDateTime now;
	{
		QDateTime incep;
		if (Q_UNLIKELY(!latestTstSigningCertTimes(msgDER, incep, expir))) {
			return false;
		}
	}

	if (dDate.isValid()) {
		now = dDate;
	} else {
		now = QDateTime::currentDateTime();
	}

	int difference = now.daysTo(expir);

	return difference < days;
}

QSslCertificate signingCert(const QByteArray &msgDER,
    QString &saId, QString &saName)
{
	struct x509_crt *x509_crt = NULL;
	void *der = NULL;
	size_t der_size;
	char *sa_id = NULL, *sa_name = NULL;

	debugFuncCall();

	if (Q_UNLIKELY(msgDER.isEmpty())) {
		return QSslCertificate();
	}

	x509_crt = raw_cms_signing_cert(msgDER.data(), msgDER.size());
	if (Q_UNLIKELY(NULL == x509_crt)) {
		return QSslCertificate();
	}

	if (Q_UNLIKELY(0 != x509_crt_to_der(x509_crt, &der, &der_size))) {
		x509_crt_destroy(x509_crt); x509_crt = NULL;
		return QSslCertificate();
	}

	if (Q_UNLIKELY(0 != x509_crt_algorithm_info(x509_crt, &sa_id, &sa_name))) {
		x509_crt_destroy(x509_crt); x509_crt = NULL;
		return QSslCertificate();
	}

	x509_crt_destroy(x509_crt); x509_crt = NULL;

	saId = sa_id;
	saName = sa_name;

	::std::free(sa_id); sa_id = NULL;
	::std::free(sa_name); sa_name = NULL;

	QByteArray certRawBytes((char *) der, der_size);
	::std::free(der); der = NULL;

	return QSslCertificate(certRawBytes, QSsl::Der);
}

bool signingCertIssuerInfo(const QByteArray &DER, QString &oStr, QString &ouStr,
    QString &nStr, QString &cStr)
{
	struct x509_crt *signing_cert = NULL;
	struct crt_issuer_info cii;

	debugFuncCall();

	crt_issuer_info_init(&cii);

	if (Q_UNLIKELY(DER.isEmpty())) {
		return false;
	}

	signing_cert = raw_cms_signing_cert(DER.data(), DER.size());
	if (Q_UNLIKELY(NULL == signing_cert)) {
		goto fail;
	}

	if (Q_UNLIKELY(0 != x509_crt_issuer_info(signing_cert, &cii))) {
		goto fail;
	}

	x509_crt_destroy(signing_cert); signing_cert = NULL;

	if (NULL != cii.o) {
		oStr = cii.o;
	}

	if (NULL != cii.ou) {
		ouStr = cii.ou;
	}

	if (NULL != cii.n) {
		nStr = cii.n;
	}

	if (NULL != cii.c) {
		cStr = cii.c;
	}

	crt_issuer_info_clear(&cii);

	return true;

fail:
	if (NULL != signing_cert) {
		x509_crt_destroy(signing_cert);
	}
	crt_issuer_info_clear(&cii);
	return false;
}

bool signingCertTimes(const QByteArray &DER, QDateTime &incTime,
    QDateTime &expTime)
{
	debugFuncCall();

	if (Q_UNLIKELY(DER.isEmpty())) {
		return false;
	}

	return _signingCertTimes(DER.data(), DER.size(), incTime, expTime);
}

bool signingCertExpiresBefore(const QByteArray &DER,
    int days, const QDateTime &dDate)
{
	QDateTime expir;
	QDateTime now;
	{
		QDateTime incep;
		if (Q_UNLIKELY(!signingCertTimes(DER, incep, expir))) {
			return false;
		}
	}

	if (dDate.isValid()) {
		now = dDate;
	} else {
		now = QDateTime::currentDateTime();
	}

	int difference = now.daysTo(expir);

	return difference < days;
}

bool signingCertValid(const QByteArray &DER, struct crt_verif_outcome &cvo)
{
	struct x509_crt *signing_cert = NULL;
	int ret;

	debugFuncCall();

	if (Q_UNLIKELY(DER.isEmpty())) {
		return false;
	}

	signing_cert = raw_cms_signing_cert(DER.data(), DER.size());
	if (Q_UNLIKELY(NULL == signing_cert)) {
		return false;
	}

	ret = x509_crt_verify(signing_cert, NULL);

	x509_crt_track_verification(signing_cert, &cvo);

	x509_crt_destroy(signing_cert); signing_cert = NULL;
	return 1 == ret;
}
