/*
 * c_signature.c - asymmetric signature class implementation
 *
 * Copyright (C) 2026 Alessandro Rinaldi
 * Copyright (C) 2013-2019 Tobias Boege <tobias@gambas-buch.de>
 *
 * 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, 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 * 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.
 * You must obey the GNU General Public License in all respects
 * for all of the code used other than OpenSSL. If you modify
 * file(s) with this exception, you may extend this exception to
 * your version of the file(s), but you are not obligated to do so.
 * If you do not wish to do so, delete this exception statement
 * from your version. If you delete this exception statement from
 * all source files in the program, then also delete it here.
 */

#define __C_SIGNATURE_C

#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/rsa.h>

#include "main.h"
#include "c_signature.h"
#include "c_pkey.h"

/* Compatibility shims for OpenSSL < 1.1.0 (Ubuntu trusty/xenial). The
 * EVP_MD_CTX_new/free names appeared in 1.1.0; before that the same
 * functions existed as EVP_MD_CTX_create/destroy. */
#if OPENSSL_VERSION_NUMBER < 0x10100000L
static EVP_MD_CTX *EVP_MD_CTX_new(void)
{
	return EVP_MD_CTX_create();
}
static void EVP_MD_CTX_free(EVP_MD_CTX *ctx)
{
	EVP_MD_CTX_destroy(ctx);
}
#endif

/* RSA_PSS_SALTLEN_DIGEST was added to <openssl/rsa.h> in 1.0.2 and is
 * missing on 1.0.1. The numeric value is the same across all versions. */
#ifndef RSA_PSS_SALTLEN_DIGEST
#define RSA_PSS_SALTLEN_DIGEST -1
#endif

/* CPKEY struct is shared via c_pkey.h. Access pkey->pkey directly
 * here since c_signature.c needs the EVP_PKEY * for sign/verify ops. */

struct sig_algo {
	const char *name;
	int         key_type;
	int         padding;
	const char *md_name;
};

static const struct sig_algo SIG_ALGOS[] = {
	{ "rsa-pkcs1-sha256", EVP_PKEY_RSA, RSA_PKCS1_PADDING,     "sha256" },
	{ "rsa-pkcs1-sha384", EVP_PKEY_RSA, RSA_PKCS1_PADDING,     "sha384" },
	{ "rsa-pkcs1-sha512", EVP_PKEY_RSA, RSA_PKCS1_PADDING,     "sha512" },
	{ "rsa-pss-sha256",   EVP_PKEY_RSA, RSA_PKCS1_PSS_PADDING, "sha256" },
	{ "rsa-pss-sha384",   EVP_PKEY_RSA, RSA_PKCS1_PSS_PADDING, "sha384" },
	{ "rsa-pss-sha512",   EVP_PKEY_RSA, RSA_PKCS1_PSS_PADDING, "sha512" },
	{ NULL, 0, 0, NULL }
};

static const struct sig_algo *sig_lookup(const char *name)
{
	int i;
	for (i = 0; SIG_ALGOS[i].name; i++) {
		if (!strcasecmp(SIG_ALGOS[i].name, name))
			return &SIG_ALGOS[i];
	}
	return NULL;
}

/* Per-virtual-object selected algorithm — set by Signature._get */
static const struct sig_algo *_algo;

/**G
 * Return a list of all signature methods supported by this build.
 * Currently RSA only: rsa-pkcs1-sha{256,384,512}, rsa-pss-sha{256,384,512}.
 **/
BEGIN_PROPERTY(Signature_List)

	GB_ARRAY arr;
	int i;
	char **slot;

	GB.Array.New(&arr, GB_T_STRING, 0);
	for (i = 0; SIG_ALGOS[i].name; i++) {
		slot = (char **) GB.Array.Add(arr);
		*slot = GB.NewZeroString(SIG_ALGOS[i].name);
	}
	GB.ReturnObject(arr);

END_PROPERTY

/**G
 * Check whether the named signature method is supported.
 **/
BEGIN_METHOD(Signature_IsSupported, GB_STRING method)

	GB.ReturnBoolean(sig_lookup(GB.ToZeroString(ARG(method))) != NULL);

END_METHOD

/**G
 * Return a virtual object representing a signature method by giving its
 * name. Valid names can be looked up from Signature.List.
 **/
BEGIN_METHOD(Signature_get, GB_STRING method)

	const struct sig_algo *a = sig_lookup(GB.ToZeroString(ARG(method)));
	if (!a) {
		GB.Error("Unknown signature method");
		return;
	}
	_algo = a;
	RETURN_SELF();

END_METHOD

/**G
 * Sign the given data with the supplied private key using this
 * signature method. Returns the raw binary signature.
 *
 * Raises an exception if Key is not a private key or if OpenSSL
 * rejects the operation.
 **/
BEGIN_METHOD(SignatureMethod_Sign, GB_STRING data; GB_OBJECT key)

	CPKEY *key_obj = (CPKEY *) VARG(key);
	const EVP_MD *md = NULL;
	EVP_MD_CTX *ctx = NULL;
	EVP_PKEY_CTX *pctx = NULL;
	unsigned char *sig = NULL;
	size_t siglen = 0;

	md = EVP_get_digestbyname(_algo->md_name);
	if (!md) { GB.Error("Digest not available"); return; }

	ctx = EVP_MD_CTX_new();
	if (!ctx) { MAIN_error("Cannot create MD context: &1"); return; }

	if (EVP_DigestSignInit(ctx, &pctx, md, NULL, key_obj->pkey) <= 0)
		goto err;

	if (_algo->padding == RSA_PKCS1_PSS_PADDING) {
		if (EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PSS_PADDING) <= 0) goto err;
		/* PSS salt length = digest size (RSA_PSS_SALTLEN_DIGEST in 1.1+) */
		if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pctx, RSA_PSS_SALTLEN_DIGEST) <= 0) goto err;
	}
	/* For RSA_PKCS1_PADDING no setup is needed: it is the default after
	 * EVP_DigestSignInit on an RSA key. */

	if (EVP_DigestSignUpdate(ctx, STRING(data), LENGTH(data)) <= 0) goto err;
	if (EVP_DigestSignFinal(ctx, NULL, &siglen) <= 0) goto err;     /* probe size */
	sig = OPENSSL_malloc(siglen);
	if (!sig) { EVP_MD_CTX_free(ctx); MAIN_error("Out of memory: &1"); return; }
	if (EVP_DigestSignFinal(ctx, sig, &siglen) <= 0) goto err;

	EVP_MD_CTX_free(ctx);
	GB.ReturnNewString((char *) sig, siglen);
	OPENSSL_free(sig);
	return;

err:
	if (sig) OPENSSL_free(sig);
	if (ctx) EVP_MD_CTX_free(ctx);
	MAIN_error("RSA sign failed: &1");

END_METHOD

/**G
 * Verify a signature against the given data using a public (or private)
 * key. The signature should have been produced by Sign() with a matching
 * private key.
 *
 * Returns:
 *   TRUE  - the signature is valid for this data and key
 *   FALSE - the signature does NOT match (data tampered, wrong key, wrong
 *           algorithm, or forged signature)
 *
 * IMPORTANT: a return value of FALSE is NOT an error - the verification
 * completed successfully and concluded that the signature is invalid.
 * The absence of an exception does NOT mean the signature is valid; you
 * MUST check the return value. Using Verify() as if it returned void
 * would silently accept any signature.
 *
 * An exception is raised only if the key is malformed, the algorithm is
 * unavailable, or an internal OpenSSL error occurs.
 **/
BEGIN_METHOD(SignatureMethod_Verify, GB_STRING data; GB_STRING sig; GB_OBJECT key)

	CPKEY *key_obj = (CPKEY *) VARG(key);
	const EVP_MD *md = NULL;
	EVP_MD_CTX *ctx = NULL;
	EVP_PKEY_CTX *pctx = NULL;
	int rc;

	md = EVP_get_digestbyname(_algo->md_name);
	if (!md) { GB.Error("Digest not available"); return; }

	ctx = EVP_MD_CTX_new();
	if (!ctx) { MAIN_error("Cannot create MD context: &1"); return; }

	if (EVP_DigestVerifyInit(ctx, &pctx, md, NULL, key_obj->pkey) <= 0)
		goto err;

	if (_algo->padding == RSA_PKCS1_PSS_PADDING) {
		if (EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PSS_PADDING) <= 0) goto err;
		if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pctx, RSA_PSS_SALTLEN_DIGEST) <= 0) goto err;
	}
	/* PKCS1 padding is the default after EVP_DigestVerifyInit on an RSA
	 * key — no explicit set call needed. */

	if (EVP_DigestVerifyUpdate(ctx, STRING(data), LENGTH(data)) <= 0)
		goto err;

	rc = EVP_DigestVerifyFinal(ctx, (unsigned char *) STRING(sig), LENGTH(sig));
	EVP_MD_CTX_free(ctx);

	if (rc == 1) {
		GB.ReturnBoolean(1);
	} else if (rc == 0) {
		ERR_clear_error();      /* signature mismatch — not an error */
		GB.ReturnBoolean(0);
	} else {
		MAIN_error("RSA verify failed: &1");
	}
	return;

err:
	if (ctx) EVP_MD_CTX_free(ctx);
	MAIN_error("RSA verify failed: &1");

END_METHOD

GB_DESC CSignature[] = {
	GB_DECLARE("Signature", 0),
	GB_NOT_CREATABLE(),

	GB_STATIC_PROPERTY_READ("List", "String[]", Signature_List),

	GB_STATIC_METHOD("_get", ".Signature.Method", Signature_get, "(Method)s"),
	GB_STATIC_METHOD("IsSupported", "b", Signature_IsSupported, "(Method)s"),

	GB_END_DECLARE
};

GB_DESC CSignatureMethod[] = {
	GB_DECLARE(".Signature.Method", 0),
	GB_VIRTUAL_CLASS(),

	GB_STATIC_METHOD("Sign",   "s", SignatureMethod_Sign,   "(Data)s(Key)PKey"),
	GB_STATIC_METHOD("Verify", "b", SignatureMethod_Verify, "(Data)s(Signature)s(Key)PKey"),

	GB_END_DECLARE
};
