//
// X509Certificate.cpp
//
// Library: Crypto
// Package: Certificate
// Module:  X509Certificate
//
// Copyright (c) 2006-2009, Applied Informatics Software Engineering GmbH.
// and Contributors.
//
// SPDX-License-Identifier:	BSL-1.0
//


#include "Poco/Crypto/X509Certificate.h"
#include "Poco/Crypto/CryptoException.h"
#include "Poco/StreamCopier.h"
#include "Poco/DateTimeParser.h"
#include "Poco/Format.h"
#include <sstream>
#include <openssl/pem.h>
#ifdef _WIN32
// fix for WIN32 header conflict
#undef X509_NAME
#endif
#include <openssl/x509v3.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/bn.h>


namespace Poco {
namespace Crypto {


namespace {

// ASN1 UTCTime format: YYMMDDHHMMSSZ (RFC 5280 Section 4.1.2.5.1)
const std::string ASN1_UTCTIME_FORMAT("%y%m%d%H%M%S%Z");
// ASN1 GeneralizedTime format: YYYYMMDDHHMMSSZ (RFC 5280 Section 4.1.2.5.2)
const std::string ASN1_GENERALIZEDTIME_FORMAT("%Y%m%d%H%M%S%Z");

} // namespace


X509Certificate::X509Certificate(std::istream& istr) : _pCert(nullptr)
{
	load(istr);
}


X509Certificate::X509Certificate(const std::string& path) : _pCert(nullptr)
{
	load(path);
}


X509Certificate::X509Certificate(X509* pCert):
	_pCert(pCert)
{
	poco_check_ptr(_pCert);

	init();
}


X509Certificate::X509Certificate(X509* pCert, bool shared):
	_pCert(pCert)
{
	poco_check_ptr(_pCert);

	if (shared)
	{
		X509_up_ref(_pCert);
	}

	init();
}


X509Certificate::X509Certificate(const X509Certificate& cert):
	_issuerName(cert._issuerName),
	_subjectName(cert._subjectName),
	_serialNumber(cert._serialNumber),
	_pCert(cert._pCert)
{
	_pCert = X509_dup(_pCert);
}


X509Certificate::X509Certificate(X509Certificate&& cert) noexcept:
	_issuerName(std::move(cert._issuerName)),
	_subjectName(std::move(cert._subjectName)),
	_serialNumber(std::move(cert._serialNumber)),
	_pCert(cert._pCert)
{
	cert._pCert = nullptr;
}


X509Certificate& X509Certificate::operator = (const X509Certificate& cert)
{
	X509Certificate tmp(cert);
	swap(tmp);
	return *this;
}


X509Certificate& X509Certificate::operator = (X509Certificate&& cert) noexcept
{
	_issuerName = std::move(cert._issuerName);
	_subjectName = std::move(cert._subjectName);
	_serialNumber = std::move(cert._serialNumber);
	if (_pCert) X509_free(_pCert);
	_pCert = cert._pCert; cert._pCert = nullptr;
	return *this;
}


void X509Certificate::swap(X509Certificate& cert) noexcept
{
	using std::swap;
	swap(cert._issuerName, _issuerName);
	swap(cert._subjectName, _subjectName);
	swap(cert._serialNumber, _serialNumber);
	swap(cert._pCert, _pCert);
}


X509Certificate::~X509Certificate()
{
	if (_pCert) X509_free(_pCert);
}


void X509Certificate::load(std::istream& istr)
{
	poco_assert (!_pCert);

	std::stringstream certStream;
	Poco::StreamCopier::copyStream(istr, certStream);
	std::string cert = certStream.str();

	BIO *pBIO = BIO_new_mem_buf(const_cast<char*>(cert.data()), static_cast<int>(cert.size()));
	if (!pBIO) throw Poco::IOException("Cannot create BIO for reading certificate");
	_pCert = PEM_read_bio_X509(pBIO, nullptr, nullptr, nullptr);
	BIO_free(pBIO);

	if (!_pCert) throw Poco::IOException("Failed to load certificate from stream");

	init();
}


void X509Certificate::load(const std::string& path)
{
	poco_assert (!_pCert);

	BIO *pBIO = BIO_new(BIO_s_file());
	if (!pBIO) throw Poco::IOException("Cannot create BIO for reading certificate file", path);
	if (!BIO_read_filename(pBIO, path.c_str()))
	{
		BIO_free(pBIO);
		throw Poco::OpenFileException("Cannot open certificate file for reading", path);
	}

	_pCert = PEM_read_bio_X509(pBIO, nullptr, nullptr, nullptr);
	BIO_free(pBIO);

	if (!_pCert) throw Poco::ReadFileException("Faild to load certificate from", path);

	init();
}


void X509Certificate::save(std::ostream& stream) const
{
	BIO *pBIO = BIO_new(BIO_s_mem());
	if (!pBIO) throw Poco::IOException("Cannot create BIO for writing certificate");
	try
	{
		if (!PEM_write_bio_X509(pBIO, _pCert))
			throw Poco::IOException("Failed to write certificate to stream");

		char *pData;
		long size;
		size = BIO_get_mem_data(pBIO, &pData);
		stream.write(pData, size);
	}
	catch (...)
	{
		BIO_free(pBIO);
		throw;
	}
	BIO_free(pBIO);
}


void X509Certificate::save(const std::string& path) const
{
	BIO *pBIO = BIO_new(BIO_s_file());
	if (!pBIO) throw Poco::IOException("Cannot create BIO for reading certificate file", path);
	if (!BIO_write_filename(pBIO, const_cast<char*>(path.c_str())))
	{
		BIO_free(pBIO);
		throw Poco::CreateFileException("Cannot create certificate file", path);
	}
	try
	{
		if (!PEM_write_bio_X509(pBIO, _pCert))
			throw Poco::WriteFileException("Failed to write certificate to file", path);
	}
	catch (...)
	{
		BIO_free(pBIO);
		throw;
	}
	BIO_free(pBIO);
}


std::string _X509_NAME_oneline_utf8(const X509_NAME *name)
{
	BIO * bio_out = BIO_new(BIO_s_mem());
	X509_NAME_print_ex(bio_out, name, 0, (ASN1_STRFLGS_RFC2253 | XN_FLAG_SEP_COMMA_PLUS | XN_FLAG_FN_SN | XN_FLAG_DUMP_UNKNOWN_FIELDS) & ~ASN1_STRFLGS_ESC_MSB);
	BUF_MEM *bio_buf;
	BIO_get_mem_ptr(bio_out, &bio_buf);
	std::string line = std::string(bio_buf->data, bio_buf->length);
	BIO_free(bio_out);
	return line;
}


void X509Certificate::init()
{
	_issuerName = _X509_NAME_oneline_utf8(X509_get_issuer_name(_pCert));
	_subjectName = _X509_NAME_oneline_utf8(X509_get_subject_name(_pCert));
	BIGNUM* pBN = ASN1_INTEGER_to_BN(X509_get_serialNumber(const_cast<X509 *>(_pCert)), nullptr);
	if (pBN)
	{
		char* pSN = BN_bn2hex(pBN);
		if (pSN)
		{
			_serialNumber = pSN;
			OPENSSL_free(pSN);
		}
		BN_free(pBN);
	}
}


std::string X509Certificate::commonName() const
{
	return subjectName(NID_COMMON_NAME);
}


std::string X509Certificate::issuerName(NID nid) const
{
	if (auto issuer = X509_get_issuer_name(_pCert))
	{
		char buffer[NAME_BUFFER_SIZE];
		if (X509_NAME_get_text_by_NID(issuer, nid, buffer, sizeof(buffer)) >= 0)
			return std::string(buffer);
	}
	return std::string();
}


std::string X509Certificate::subjectName(NID nid) const
{
	if (auto subj = X509_get_subject_name(_pCert))
	{
		char buffer[NAME_BUFFER_SIZE];
		if (X509_NAME_get_text_by_NID(subj, nid, buffer, sizeof(buffer)) >= 0)
			return std::string(buffer);
	}
	return std::string();
}


void X509Certificate::extractNames(std::string& cmnName, std::set<std::string>& domainNames) const
{
	domainNames.clear();
	if (STACK_OF(GENERAL_NAME) *names = static_cast<STACK_OF(GENERAL_NAME) *>( X509_get_ext_d2i(_pCert, NID_subject_alt_name, nullptr, nullptr))) {
		for (int i = 0; i < sk_GENERAL_NAME_num(names); ++i)
		{
			const GENERAL_NAME* name = sk_GENERAL_NAME_value(names, i);
			if (name->type == GEN_DNS)
			{
				const char* data = reinterpret_cast<const char*>(ASN1_STRING_get0_data(name->d.ia5));
				std::size_t len = ASN1_STRING_length(name->d.ia5);
				domainNames.insert(std::string(data, len));
			}
		}
		GENERAL_NAMES_free(names);
	}

	cmnName = commonName();
	if (!cmnName.empty() && domainNames.empty())
	{
		domainNames.insert(cmnName);
	}
}


Poco::DateTime X509Certificate::validFrom() const
{
	const ASN1_TIME* certTime = X509_get0_notBefore(_pCert);
	auto certTimeType = ASN1_STRING_type(certTime);
	std::string dateTime(reinterpret_cast<const char*>(ASN1_STRING_get0_data(certTime)), ASN1_STRING_length(certTime));
	int tzd;
	if (certTimeType == V_ASN1_UTCTIME)
	{
		return DateTimeParser::parse(ASN1_UTCTIME_FORMAT, dateTime, tzd);
	}
	else if (certTimeType == V_ASN1_GENERALIZEDTIME)
	{
		return DateTimeParser::parse(ASN1_GENERALIZEDTIME_FORMAT, dateTime, tzd);
	}
	else
	{
		throw NotImplementedException("Unsupported date/time format in notBefore");
	}
}


Poco::DateTime X509Certificate::expiresOn() const
{
	const ASN1_TIME* certTime = X509_get0_notAfter(_pCert);
	auto certTimeType = ASN1_STRING_type(certTime);
	std::string dateTime(reinterpret_cast<const char*>(ASN1_STRING_get0_data(certTime)), ASN1_STRING_length(certTime));
	int tzd;
	if (certTimeType == V_ASN1_UTCTIME)
	{
		return DateTimeParser::parse(ASN1_UTCTIME_FORMAT, dateTime, tzd);
	}
	else if (certTimeType == V_ASN1_GENERALIZEDTIME)
	{
		return DateTimeParser::parse(ASN1_GENERALIZEDTIME_FORMAT, dateTime, tzd);
	}
	else
	{
		throw NotImplementedException("Unsupported date/time format in notBefore");
	}
}


Poco::DigestEngine::Digest X509Certificate::fingerprint(const std::string& algorithm) const
{
	unsigned char buffer[EVP_MAX_MD_SIZE];
	unsigned int length;
	const EVP_MD* md = EVP_get_digestbyname(algorithm.c_str());
	if (!md) throw Poco::InvalidArgumentException(algorithm);

	if (X509_digest(_pCert, md, buffer, &length))
	{
		return Poco::DigestEngine::Digest(buffer, buffer + length);
	}
	else
	{
		throw OpenSSLException("failed to compute fingerprint");
	}
}


bool X509Certificate::issuedBy(const X509Certificate& issuerCertificate) const
{
	X509* pCert = const_cast<X509*>(_pCert);
	X509* pIssuerCert = const_cast<X509*>(issuerCertificate.certificate());
	EVP_PKEY* pIssuerPublicKey = X509_get_pubkey(pIssuerCert);
	if (!pIssuerPublicKey) throw Poco::InvalidArgumentException("Issuer certificate has no public key");
	int rc = X509_verify(pCert, pIssuerPublicKey);
	EVP_PKEY_free(pIssuerPublicKey);
	return rc == 1;
}


bool X509Certificate::equals(const X509Certificate& otherCertificate) const
{
	X509* pCert = const_cast<X509*>(_pCert);
	X509* pOtherCert = const_cast<X509*>(otherCertificate.certificate());
	return X509_cmp(pCert, pOtherCert) == 0;
}


std::string X509Certificate::signatureAlgorithm() const
{
	int sigNID = X509_get_signature_nid(_pCert);

	if (sigNID != NID_undef)
	{
		const char* pAlgName = OBJ_nid2ln(sigNID);
		if (pAlgName) return std::string(pAlgName);
		else throw OpenSSLException(Poco::format("X509Certificate::"
				"signatureAlgorithm(): OBJ_nid2ln(%d)", sigNID));
	}
	else
		throw NotFoundException("X509Certificate::signatureAlgorithm()");

	return "";
}


X509Certificate::List X509Certificate::readPEM(const std::string& pemFileName)
{
	List caCertList;
	BIO* pBIO = BIO_new_file(pemFileName.c_str(), "r");
	if (pBIO == nullptr)
		throw OpenFileException(Poco::format("X509Certificate::readPEM(%s)", pemFileName));
	X509* x = PEM_read_bio_X509(pBIO, nullptr, nullptr, nullptr);
	if (!x)
	{
		BIO_free(pBIO);
		throw OpenSSLException(Poco::format("X509Certificate::readPEM(%s)", pemFileName));
	}
	while (x)
	{
		caCertList.push_back(X509Certificate(x));
		x = PEM_read_bio_X509(pBIO, nullptr, nullptr, nullptr);
	}
	BIO_free(pBIO);
	return caCertList;
}


void X509Certificate::writePEM(const std::string& pemFileName, const List& list)
{
	BIO* pBIO = BIO_new_file(pemFileName.c_str(), "a");
	if (pBIO == nullptr) throw OpenFileException(Poco::format("X509Certificate::writePEM(%s)", pemFileName));
	List::const_iterator it = list.begin();
	List::const_iterator end = list.end();
	for (; it != end; ++it)
	{
		if (!PEM_write_bio_X509(pBIO, const_cast<X509*>(it->certificate())))
		{
			BIO_free(pBIO);
			throw OpenSSLException(Poco::format("X509Certificate::writePEM(%s)", pemFileName));
		}
	}
	BIO_free(pBIO);
}


void X509Certificate::print(std::ostream& out) const
{
	out << "subjectName: " << subjectName() << std::endl;
	out << "issuerName: " << issuerName() << std::endl;
	out << "commonName: " << commonName() << std::endl;
	out << "country: " << subjectName(X509Certificate::NID_COUNTRY) << std::endl;
	out << "localityName: " << subjectName(X509Certificate::NID_LOCALITY_NAME) << std::endl;
	out << "stateOrProvince: " << subjectName(X509Certificate::NID_STATE_OR_PROVINCE) << std::endl;
	out << "organizationName: " << subjectName(X509Certificate::NID_ORGANIZATION_NAME) << std::endl;
	out << "organizationUnitName: " << subjectName(X509Certificate::NID_ORGANIZATION_UNIT_NAME) << std::endl;
	out << "emailAddress: " << subjectName(X509Certificate::NID_PKCS9_EMAIL_ADDRESS) << std::endl;
	out << "serialNumber: " << subjectName(X509Certificate::NID_SERIAL_NUMBER) << std::endl;
}


} } // namespace Poco::Crypto
