/*
 * Copyright (C) 2023 Andreas Steffen, strongSec GmbH
 *
 * Copyright (C) secunet Security Networks AG
 *
 * 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 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#include <errno.h>
#include <time.h>

#include "pki.h"
#include "ocsp/index_responder.h"

#include <collections/array.h>
#include <credentials/sets/mem_cred.h>
#include <credentials/certificates/ocsp_request.h>
#include <credentials/certificates/ocsp_response.h>
#include <credentials/certificates/ocsp_single_response.h>

/*
 * Verifies the optional OCSP request signature generated by an OCSP requestor
 */
static bool verify_signature(certificate_t *ocsp_req, mem_cred_t *creds)
{
	identification_t *signer;
	certificate_t *signer_cert, *cert_found;
	ocsp_request_t *ocsp_request;
	enumerator_t *certs;
	bool trusted = TRUE;

	ocsp_request = (ocsp_request_t*)ocsp_req;

	signer_cert = ocsp_request->get_signer_cert(ocsp_request);
	if (signer_cert)
	{
		signer = signer_cert->get_subject(signer_cert);

		/* establish trust relative to root CA */
		creds->add_cert(creds, FALSE, signer_cert->get_ref(signer_cert));
		certs = lib->credmgr->create_trusted_enumerator(lib->credmgr,
											KEY_ANY, signer, FALSE);
		trusted = certs->enumerate(certs, &cert_found, NULL) &&
								  (cert_found == signer_cert);
		certs->destroy(certs);
		trusted = trusted && ocsp_req->issued_by(ocsp_req, signer_cert, NULL);
	}
	DBG1(DBG_APP, "requestor is %strusted", trusted ? "" : "not ");

	return trusted;
}

/*
 * Find the CA certificate of the certificate issuer based on the issuerKey and
 * issuerName hashes contained in the OCSP request
 */
static bool find_issuer_cacert(hash_algorithm_t hashAlgorithm,
							   chunk_t issuerKeyHash, chunk_t issuerNameHash,
							   certificate_t **issuer_cacert)
{
	bool issuerKeyHash_ok = FALSE, issuerNameHash_ok = FALSE;
	certificate_t *candidate;
	identification_t *issuer;
	public_key_t *public;
	x509_t *x509_ca;
	chunk_t caKeyHash = chunk_empty, caNameHash = chunk_empty;
	hasher_t *hasher;
	enumerator_t *certs;

	*issuer_cacert = NULL;

	if (hashAlgorithm != HASH_SHA1)
	{
		return FALSE;
	}
	certs = lib->credmgr->create_cert_enumerator(lib->credmgr, CERT_X509,
												 KEY_ANY, NULL, TRUE);
	while (certs->enumerate(certs, &candidate))
	{
		x509_ca = (x509_t*)candidate;

		if (!(x509_ca->get_flags(x509_ca) & X509_CA))
		{
			continue;
		}

		/* retrieve the caKeyHash of the candidate issuer */
		public = candidate->get_public_key(candidate);
		if (!public)
		{
			DBG1(DBG_APP, "could not retrieve public key of cacert candidate");
			break;
		}
		if (!public->get_fingerprint(public, KEYID_PUBKEY_SHA1, &caKeyHash))
		{
			DBG1(DBG_APP, "could not retrieve SHA1 hash of public key");
			public->destroy(public);
			break;
		}

		issuerKeyHash_ok = chunk_equals_const(issuerKeyHash, caKeyHash);
		public->destroy(public);
		if (!issuerKeyHash_ok)
		{
			continue;
		}

		/* compute the caNameHash of the candidate issuer */
		issuer = candidate->get_subject(candidate);

		hasher = lib->crypto->create_hasher(lib->crypto, HASH_SHA1);
		if (!hasher)
		{
			DBG1(DBG_APP, "failed to create SHA1 hasher");
			break;
		}

		if (!hasher->allocate_hash(hasher, issuer->get_encoding(issuer),
										   &caNameHash))
		{
			hasher->destroy(hasher);
			DBG1(DBG_APP, "failed to compute SHA1 caNameHash");
			break;
		}
		hasher->destroy(hasher);

		issuerNameHash_ok = chunk_equals_const(issuerNameHash, caNameHash);
		chunk_free(&caNameHash);
		if (!issuerNameHash_ok)
		{
			continue;
		}

		/* we found the issuer */
		DBG1(DBG_APP, "issuer:             \"%Y\"", issuer);
		*issuer_cacert = candidate;

		break;
	}
	certs->destroy(certs);

	DBG1(DBG_APP, "  issuerKeyHash:     %#B (%s)", &issuerKeyHash,
					 issuerKeyHash_ok ? "ok" : "no match");
	DBG1(DBG_APP, "  issuerNameHash:    %#B (%s)", &issuerNameHash,
					 issuerNameHash_ok ? "ok" : "no match");

	return (*issuer_cacert != NULL);
}

/*
 * Find an OCSP signer certificate. Either the certificate of the CA itself that
 * issued the end-entity certificate, the certificate of an OCSP signer
 * delegated by the CA via the standard OCSPSigning Extended Key Usage (EKU)
 * flag or a self-signed OCSP signer certificate when multiple issuer OCSP
 * requests have to be supported.
 */
static void find_ocsp_signer(certificate_t *first_issuer, bool *self_signed,
							 certificate_t **cert, private_key_t **key)
{
	certificate_t *candidate;
	identification_t *keyid;
	private_key_t *key_candidate;
	chunk_t subKeyId, authKeyId, subKeyId_ca;
	x509_t *x509, *x509_ca;
	x509_flag_t flags;
	enumerator_t *certs;

	/* retrieve the subjectKeyIdentifier of the first issuer certificate */
	x509_ca = (x509_t*)first_issuer;
	subKeyId_ca = x509_ca->get_subjectKeyIdentifier(x509_ca);

	/* iterate over all certificates */
	certs = lib->credmgr->create_cert_enumerator(lib->credmgr, CERT_X509,
												 KEY_ANY, NULL, TRUE);
	while (certs->enumerate(certs, &candidate))
	{
		/* get the flags and key identifiers of the candidate certificate */
		x509 = (x509_t*)candidate;
		flags = x509->get_flags(x509);
		subKeyId  = x509->get_subjectKeyIdentifier(x509);
		authKeyId = x509->get_authKeyIdentifier(x509);

		/* get a private key matching the candidate certificate */
		keyid = identification_create_from_encoding(ID_KEY_ID, subKeyId);
		key_candidate = lib->credmgr->get_private(lib->credmgr,
										KEY_ANY, keyid, NULL);
		keyid->destroy(keyid);

		if (key_candidate)
		{
			if (((flags & X509_OCSP_SIGNER && chunk_equals(authKeyId, subKeyId_ca)) ||
				 (flags & X509_CA && chunk_equals(subKeyId, subKeyId_ca))) && !*key)
			{
				*cert = candidate;
				*key = key_candidate;
				continue;
			}
			else if (flags & X509_SELF_SIGNED && !(flags & X509_CA))
			{
				DESTROY_IF(*key);
				*cert = candidate;
				*key = key_candidate;
				*self_signed = TRUE;
				break;
			}
			key_candidate->destroy(key_candidate);
		}
	}
	certs->destroy(certs);
}

/**
 * Show|Respond to OCSP requests
 */
static int ocsp()
{
	char *arg, *file = NULL, *error = NULL;
	identification_t *requestor;
	cred_encoding_type_t form = CERT_ASN1_DER;
	private_key_t *key = NULL;
	certificate_t *cert = NULL, *ocsp_req = NULL, *ocsp_resp = NULL;
	certificate_t *cacert = NULL, *first_issuer = NULL;
	ocsp_request_t *ocsp_request;
	ocsp_status_t ocsp_status = OCSP_SUCCESSFUL;
	ocsp_responder_t *index_responder = NULL;
	linked_list_t *responses = NULL;
	array_t *index_responders = NULL;
	chunk_t encoding = chunk_empty, nonce = chunk_empty;
	chunk_t issuerNameHash, issuerKeyHash, serialNumber;
	hash_algorithm_t hashAlgorithm = HASH_SHA1, digest = HASH_UNKNOWN;
	signature_params_t *scheme = NULL;
	time_t lifetime = 0;
	mem_cred_t *creds;
	bool multiple_issuers = FALSE, self_signed = FALSE;
	enumerator_t *enumerator;
	int res = 1;

	enum {
		OP_SHOW,
		OP_RESPOND,
	} op = OP_SHOW;

	bool pss = lib->settings->get_bool(lib->settings, "%s.rsa_pss", FALSE,
									   lib->ns);

	creds = mem_cred_create();

	while (TRUE)
	{
		switch (command_getopt(&arg))
		{
			case 'h':
				goto usage;
			case 'i':
				file = arg;
				continue;
			case 'r':
				op = OP_RESPOND;
				continue;
			case 'k':
				key = lib->creds->create(lib->creds,
										 CRED_PRIVATE_KEY, KEY_ANY,
										 BUILD_FROM_FILE, arg, BUILD_END);
				if (!key)
				{
					error = "parsing private key failed";
					goto usage;
				}
				creds->add_key(creds, key);
				continue;
			case 'c':
				cert = lib->creds->create(lib->creds,
										  CRED_CERTIFICATE, CERT_X509,
										  BUILD_FROM_FILE, arg, BUILD_END);
				if (!cert)
				{
					error = "parsing certificate failed";
					goto usage;
				}
				creds->add_cert(creds, TRUE, cert);
				continue;
			case 'C':
				DESTROY_IF(cacert);
				cacert = lib->creds->create(lib->creds,
											CRED_CERTIFICATE, CERT_X509,
											BUILD_FROM_FILE, arg, BUILD_END);
				if (!cacert)
				{
					error = "parsing CA certificate failed";
					goto usage;
				}
				cacert = creds->add_cert_ref(creds, TRUE, cacert);
				continue;
			case 'l':
				lifetime = atoi(arg) * 60;
				if (!lifetime)
				{
					error = "invalid --lifetime value";
					goto usage;
				}
				continue;
			case 'g':
				if (!enum_from_name(hash_algorithm_short_names, arg, &digest))
				{
					error = "invalid --digest type";
					goto usage;
				}
				continue;
			case 'R':
				if (!parse_rsa_padding(arg, &pss))
				{
					error = "invalid RSA padding";
					goto usage;
				}
				continue;
			case 'x':
				if (!cacert)
				{
					error = "--index must follow --cacert of corresponding CA";
					goto usage;
				}
				index_responder = index_responder_create(cacert, arg);
				if (!index_responder)
				{
					error = "invalid ---index value";
					goto usage;
				}
				array_insert_create(&index_responders, ARRAY_TAIL,
									index_responder);
				continue;
			case EOF:
				break;
			default:
				error = "invalid --ocsp option";
				goto usage;
		}
		break;
	}

	lib->credmgr->add_local_set(lib->credmgr, &creds->set, FALSE);

	responses = linked_list_create();

	if (op == OP_RESPOND && !cacert)
	{
		error = "respond mode requires a CA certificate";
		goto end;
	}

	if (op == OP_RESPOND && !key)
	{
		error = "respond mode requires a private signer key";
		goto end;
	}

	/* re-initialize signing certificate and key pointers */
	cert = NULL;
	key = NULL;

	if (file)
	{
		ocsp_req = lib->creds->create(lib->creds, CRED_CERTIFICATE,
									  CERT_X509_OCSP_REQUEST,
									  BUILD_FROM_FILE, file, BUILD_END);
	}
	else
	{
		chunk_t chunk;

		set_file_mode(stdin, CERT_ASN1_DER);
		if (!chunk_from_fd(0, &chunk))
		{
			fprintf(stderr, "%s: ", strerror(errno));
			error = "reading certificate request failed";
			goto end;
		}
		ocsp_req = lib->creds->create(lib->creds, CRED_CERTIFICATE,
									  CERT_X509_OCSP_REQUEST,
									  BUILD_BLOB_ASN1_DER, chunk, BUILD_END);
		free(chunk.ptr);
	}
	if (!ocsp_req)
	{
		if (op == OP_SHOW)
		{
			error = "malformed OCSP request";
			goto end;
		}
		else
		{
			DBG1(DBG_APP, "malformed OCSP request");
			ocsp_status = OCSP_MALFORMEDREQUEST;
			goto gen;
		}
	}
	ocsp_request = (ocsp_request_t*)ocsp_req;

	/* does the requestor identify itself? */
	requestor = ocsp_req->get_subject(ocsp_req);
	if (requestor)
	{
		DBG1(DBG_APP, "requestor:          \"%Y\"", requestor);

		/* verify an optional ocsp request signature */
		if (!verify_signature(ocsp_req, creds))
		{
			ocsp_status = OCSP_UNAUTHORIZED;
			goto gen;
		}
	}

	/* extract nonce from OCSP request */
	nonce = ocsp_request->get_nonce(ocsp_request);
	if (nonce.len > 0)
	{
		DBG1(DBG_APP, "nonce:               %#B", &nonce);
	}

	/* enumerate over the ocsp requests and try to identify the issuers */
	enumerator = ocsp_request->create_request_enumerator(ocsp_request);
	while (enumerator->enumerate(enumerator, &hashAlgorithm, &issuerNameHash,
											 &issuerKeyHash, &serialNumber))
	{
		certificate_t *issuer_cacert = NULL;
		cert_validation_t status = VALIDATION_FAILED;
		ocsp_single_response_t *response = NULL;
		crl_reason_t revocationReason;
		time_t revocationTime;

		/* search for the matching issuer cacert */
		if (find_issuer_cacert(hashAlgorithm, issuerKeyHash, issuerNameHash,
							   &issuer_cacert))
		{
			if (!first_issuer)
			{
				first_issuer = issuer_cacert;

				/* search for a signing certificate plus matching private key */
				if (op == OP_RESPOND)
				{
					find_ocsp_signer(first_issuer, &self_signed, &cert, &key);
				}
			}
			else if (first_issuer != issuer_cacert)
			{
				multiple_issuers = TRUE;
			}
		}
		DBG1(DBG_APP, "  serialNumber:      %#B", &serialNumber);

		if (op == OP_SHOW)
		{
			continue;
		}

		/**
		 *  fill in the OCSP single response
		 */
		response = ocsp_single_response_create();
		response->hashAlgorithm  = hashAlgorithm;
		response->issuerNameHash = chunk_clone(issuerNameHash);
		response->issuerKeyHash  = chunk_clone(issuerKeyHash);
		response->serialNumber   = chunk_clone(serialNumber);
		response->thisUpdate     = time(NULL);
		DBG1(DBG_APP, "  thisUpdate:        %#T", &response->thisUpdate, TRUE);

		if (lifetime)
		{
			response->nextUpdate = response->thisUpdate + lifetime;
			DBG1(DBG_APP, "  nextUpdate:        %#T", &response->nextUpdate, TRUE);
		}

		if (issuer_cacert && (issuer_cacert == first_issuer || self_signed))
		{
			status = lib->ocsp->get_status(lib->ocsp,
										   issuer_cacert,	serialNumber,
										   &revocationTime, &revocationReason);
		}
		DBG1(DBG_APP, "  certValidation:    %N", cert_validation_names, status);
		response->status = status;

		if (status == VALIDATION_REVOKED || status == VALIDATION_ON_HOLD)
		{
			DBG1(DBG_APP, "  revocationTime:    %T", &revocationTime, TRUE);
			DBG1(DBG_APP, "  revocationReason:  %N", crl_reason_names,
												  revocationReason);
			response->revocationTime   = revocationTime;
			response->revocationReason = revocationReason;
		}
		responses->insert_last(responses, response);
	}
	enumerator->destroy(enumerator);

	if (multiple_issuers)
	{
		DBG1(DBG_APP, "there are multiple known issuers");
	}

gen:
	if (op == OP_RESPOND)
	{
		if (cert)
		{
			DBG1(DBG_APP, "%s \"%Y\"",
				 self_signed ? "self-signed signer:" : "trusted signer:    ",
				 cert->get_subject(cert));

			scheme = get_signature_scheme(key, digest, pss);
			if (scheme)
			{
				if (digest == HASH_UNKNOWN)
				{
					digest = hasher_from_signature_scheme(scheme->scheme,
														  scheme->params);
				}
			}
			else
			{
				DBG1(DBG_APP, "no signature scheme found");
				ocsp_status = OCSP_INTERNALERROR;
			}
		}
		else
		{
			DBG1(DBG_APP, "no signer certificate found");
			ocsp_status = OCSP_INTERNALERROR;
		}
		DBG1(DBG_APP, "ocspResponseStatus:  %N", ocsp_status_names, ocsp_status);

		enumerator = responses->create_enumerator(responses);
		ocsp_resp = lib->creds->create(lib->creds, CRED_CERTIFICATE,
						CERT_X509_OCSP_RESPONSE,
						BUILD_OCSP_STATUS, ocsp_status,
						BUILD_OCSP_RESPONSES, enumerator,
						BUILD_SIGNING_KEY, key,
						BUILD_SIGNING_CERT, cert,
						BUILD_SIGNATURE_SCHEME, scheme,
						BUILD_NONCE, nonce,
						BUILD_END);
		enumerator->destroy(enumerator);

		if (!ocsp_resp)
		{
			error = "generating OCSP response failed";
			goto end;
		}
		if (!ocsp_resp->get_encoding(ocsp_resp, form, &encoding))
		{
			error = "encoding OCSP response failed";
			goto end;
		}
		set_file_mode(stdout, form);
		if (fwrite(encoding.ptr, encoding.len, 1, stdout) != 1)
		{
			error = "writing OCSP response failed";
			goto end;
		}
	}
	res = 0;

end:
	array_destroy_offset(index_responders, offsetof(ocsp_responder_t, destroy));
	DESTROY_IF(cacert);
	DESTROY_IF(key);
	lib->credmgr->remove_local_set(lib->credmgr, &creds->set);
	creds->destroy(creds);
	responses->destroy_offset(responses,
							  offsetof(ocsp_single_response_t, destroy));
	DESTROY_IF(ocsp_req);
	DESTROY_IF(ocsp_resp);
	signature_params_destroy(scheme);
	free(encoding.ptr);
	if (error)
	{
		fprintf(stderr, "%s\n", error);
	}
	return res;

usage:
	array_destroy_offset(index_responders, offsetof(ocsp_responder_t, destroy));
	DESTROY_IF(cacert);
	creds->destroy(creds);
	return command_usage(error);
}

/**
 * Register the command.
 */
static void __attribute__ ((constructor))reg()
{
	command_register((command_t) {
		ocsp, 'o', "ocsp", "OCSP responder",
		{"[--in file] [--respond] [--cert file]+ [--key file]+ ",
		 "[--cacert file [--index file]]+",
		 "[--digest md5|sha1|sha224|sha256|sha384|sha512|sha3_224|sha3_256|sha3_384|sha3_512]",
		 "[--rsa-padding pkcs1|pss] [--lifetime minutes]"},
		{
			{"help",          'h', 0, "show usage information"},
			{"respond",       'r', 0, "respond to OCSP request with OCSP response"},
			{"in",            'i', 1, "input file, default: stdin"},
			{"key",           'k', 1, "path to OCSP signing private key (can be used multiple times)"},
			{"cert",          'c', 1, "path to OCSP signing certificate (can be used multiple times"},
			{"cacert",        'C', 1, "CA certificate (can be used multiple times"},
			{"index",         'x', 1, "OpenSSL-style index.txt to check status of certificates"},
			{"digest",        'g', 1, "digest for signature creation, default: key-specific"},
			{"rsa-padding",   'R', 1, "padding for RSA signatures, default: pkcs1"},
			{"lifetime",      'l', 1, "validity in minutes of the OCSP response (if missing, nextUpdate is omitted)"},
		}
	});
}
