/* Sign a module file using the given key.
 *
 * Copyright © 2014-2016 Red Hat, Inc. All Rights Reserved.
 * Copyright © 2015      Intel Corporation.
 * Copyright © 2016      Hewlett Packard Enterprise Development LP
 *
 * Authors: David Howells <dhowells@redhat.com>
 *          David Woodhouse <dwmw2@infradead.org>
 *          Juerg Haefliger <juerg.haefliger@hpe.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the licence, or (at your option) any later version.
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <getopt.h>
#include <err.h>
#include <arpa/inet.h>
#include <openssl/opensslv.h>
#include <openssl/bio.h>
#include <openssl/cms.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
# define USE_PKCS11_PROVIDER
# include <openssl/provider.h>
# include <openssl/store.h>
#else
# if !defined(OPENSSL_NO_ENGINE) && !defined(OPENSSL_NO_DEPRECATED_3_0)
#  define USE_PKCS11_ENGINE
#  include <openssl/engine.h>
# endif
#endif
#include "ssl-common.h"

#include <linux/module_signature.h>

static __attribute__((noreturn))
void format(void)
{
	fprintf(stderr,
		"Usage: scripts/sign-file [-dp] <hash algo> <key> <x509> <module> [<dest>]\n");
	fprintf(stderr,
		"       scripts/sign-file -s <raw sig> <hash algo> <x509> <module> [<dest>]\n");
	exit(2);
}

static const char *key_pass;

static int pem_pw_cb(char *buf, int len, int w, void *v)
{
	int pwlen;

	if (!key_pass)
		return -1;

	pwlen = strlen(key_pass);
	if (pwlen >= len)
		return -1;

	strcpy(buf, key_pass);

	/* If it's wrong, don't keep trying it. */
	key_pass = NULL;

	return pwlen;
}

static EVP_PKEY *read_private_key_pkcs11(const char *private_key_name)
{
	EVP_PKEY *private_key = NULL;
#ifdef USE_PKCS11_PROVIDER
	OSSL_STORE_CTX *store;

	if (!OSSL_PROVIDER_try_load(NULL, "pkcs11", true))
		ERR(1, "OSSL_PROVIDER_try_load(pkcs11)");
	if (!OSSL_PROVIDER_try_load(NULL, "default", true))
		ERR(1, "OSSL_PROVIDER_try_load(default)");

	store = OSSL_STORE_open(private_key_name, NULL, NULL, NULL, NULL);
	ERR(!store, "OSSL_STORE_open");

	while (!OSSL_STORE_eof(store)) {
		OSSL_STORE_INFO *info = OSSL_STORE_load(store);

		if (!info) {
			drain_openssl_errors(__LINE__, 0);
			continue;
		}
		if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) {
			private_key = OSSL_STORE_INFO_get1_PKEY(info);
			ERR(!private_key, "OSSL_STORE_INFO_get1_PKEY");
		}
		OSSL_STORE_INFO_free(info);
		if (private_key)
			break;
	}
	OSSL_STORE_close(store);
#elif defined(USE_PKCS11_ENGINE)
	ENGINE *e;

	ENGINE_load_builtin_engines();
	drain_openssl_errors(__LINE__, 1);
	e = ENGINE_by_id("pkcs11");
	ERR(!e, "Load PKCS#11 ENGINE");
	if (ENGINE_init(e))
		drain_openssl_errors(__LINE__, 1);
	else
		ERR(1, "ENGINE_init");
	if (key_pass)
		ERR(!ENGINE_ctrl_cmd_string(e, "PIN", key_pass, 0), "Set PKCS#11 PIN");
	private_key = ENGINE_load_private_key(e, private_key_name, NULL, NULL);
	ERR(!private_key, "%s", private_key_name);
#else
	fprintf(stderr, "no pkcs11 engine/provider available\n");
	exit(1);
#endif
	return private_key;
}

static EVP_PKEY *read_private_key(const char *private_key_name)
{
	if (!strncmp(private_key_name, "pkcs11:", 7)) {
		return read_private_key_pkcs11(private_key_name);
	} else {
		EVP_PKEY *private_key;
		BIO *b;

		b = BIO_new_file(private_key_name, "rb");
		ERR(!b, "%s", private_key_name);
		private_key = PEM_read_bio_PrivateKey(b, NULL, pem_pw_cb,
						      NULL);
		ERR(!private_key, "%s", private_key_name);
		BIO_free(b);

		return private_key;
	}
}

static X509 *read_x509(const char *x509_name)
{
	unsigned char buf[2];
	X509 *x509;
	BIO *b;
	int n;

	b = BIO_new_file(x509_name, "rb");
	ERR(!b, "%s", x509_name);

	/* Look at the first two bytes of the file to determine the encoding */
	n = BIO_read(b, buf, 2);
	if (n != 2) {
		if (BIO_should_retry(b)) {
			fprintf(stderr, "%s: Read wanted retry\n", x509_name);
			exit(1);
		}
		if (n >= 0) {
			fprintf(stderr, "%s: Short read\n", x509_name);
			exit(1);
		}
		ERR(1, "%s", x509_name);
	}

	ERR(BIO_reset(b) != 0, "%s", x509_name);

	if (buf[0] == 0x30 && buf[1] >= 0x81 && buf[1] <= 0x84)
		/* Assume raw DER encoded X.509 */
		x509 = d2i_X509_bio(b, NULL);
	else
		/* Assume PEM encoded X.509 */
		x509 = PEM_read_bio_X509(b, NULL, NULL, NULL);

	BIO_free(b);
	ERR(!x509, "%s", x509_name);

	return x509;
}

int main(int argc, char **argv)
{
	struct module_signature sig_info = { .id_type = MODULE_SIGNATURE_TYPE_PKCS7 };
	char *hash_algo = NULL;
	char *private_key_name = NULL, *raw_sig_name = NULL;
	char *x509_name, *module_name, *dest_name;
	bool save_sig = false, replace_orig;
	bool sign_only = false;
	bool raw_sig = false;
	unsigned char buf[4096];
	unsigned long module_size, sig_size;
	const EVP_MD *digest_algo;
	EVP_PKEY *private_key;
	CMS_ContentInfo *cms = NULL;
	unsigned int use_keyid = 0;
	X509 *x509;
	BIO *bd, *bm;
	int opt, n;
	OpenSSL_add_all_algorithms();
	ERR_load_crypto_strings();
	ERR_clear_error();

	key_pass = getenv("KBUILD_SIGN_PIN");

	do {
		opt = getopt(argc, argv, "sdpk");
		switch (opt) {
		case 's': raw_sig = true; break;
		case 'p': save_sig = true; break;
		case 'd': sign_only = true; save_sig = true; break;
		case 'k': use_keyid = CMS_USE_KEYID; break;
		case -1: break;
		default: format();
		}
	} while (opt != -1);

	argc -= optind;
	argv += optind;
	if (argc < 4 || argc > 5)
		format();

	if (raw_sig) {
		raw_sig_name = argv[0];
		hash_algo = argv[1];
	} else {
		hash_algo = argv[0];
		private_key_name = argv[1];
	}
	x509_name = argv[2];
	module_name = argv[3];
	if (argc == 5 && strcmp(argv[3], argv[4]) != 0) {
		dest_name = argv[4];
		replace_orig = false;
	} else {
		ERR(asprintf(&dest_name, "%s.~signed~", module_name) < 0,
		    "asprintf");
		replace_orig = true;
	}

	/* Open the module file */
	bm = BIO_new_file(module_name, "rb");
	ERR(!bm, "%s", module_name);

	if (!raw_sig) {
		/* Read the private key and the X.509 cert the PKCS#7 message
		 * will point to.
		 */
		private_key = read_private_key(private_key_name);
		x509 = read_x509(x509_name);

		/* Digest the module data. */
		OpenSSL_add_all_digests();
		drain_openssl_errors(__LINE__, 0);
		digest_algo = EVP_get_digestbyname(hash_algo);
		ERR(!digest_algo, "EVP_get_digestbyname");

		unsigned int flags =
			CMS_NOCERTS |
			CMS_NOATTR |
			CMS_PARTIAL |
			CMS_BINARY |
			CMS_DETACHED |
			CMS_STREAM  |
			CMS_NOSMIMECAP |
#ifdef CMS_NO_SIGNING_TIME
			CMS_NO_SIGNING_TIME |
#endif
			use_keyid;

#if OPENSSL_VERSION_NUMBER >= 0x30000000L && OPENSSL_VERSION_NUMBER < 0x40000000L
		if (EVP_PKEY_is_a(private_key, "ML-DSA-44") ||
		    EVP_PKEY_is_a(private_key, "ML-DSA-65") ||
		    EVP_PKEY_is_a(private_key, "ML-DSA-87")) {
			 /* ML-DSA + CMS_NOATTR is not supported in openssl-3.5
			  * and before.
			  */
			flags &= ~CMS_NOATTR;
		}
#endif

		/* Load the signature message from the digest buffer. */
		cms = CMS_sign(NULL, NULL, NULL, NULL, flags);
		ERR(!cms, "CMS_sign");

		ERR(!CMS_add1_signer(cms, x509, private_key, digest_algo, flags),
		    "CMS_add1_signer");
		ERR(CMS_final(cms, bm, NULL, flags) != 1,
		    "CMS_final");

		if (save_sig) {
			char *sig_file_name;
			BIO *b;

			ERR(asprintf(&sig_file_name, "%s.p7s", module_name) < 0,
			    "asprintf");
			b = BIO_new_file(sig_file_name, "wb");
			ERR(!b, "%s", sig_file_name);
			ERR(i2d_CMS_bio_stream(b, cms, NULL, 0) != 1,
			    "%s", sig_file_name);
			BIO_free(b);
		}

		if (sign_only) {
			BIO_free(bm);
			return 0;
		}
	}

	/* Open the destination file now so that we can shovel the module data
	 * across as we read it.
	 */
	bd = BIO_new_file(dest_name, "wb");
	ERR(!bd, "%s", dest_name);

	/* Append the marker and the PKCS#7 message to the destination file */
	ERR(BIO_reset(bm) < 0, "%s", module_name);
	while ((n = BIO_read(bm, buf, sizeof(buf))),
	       n > 0) {
		ERR(BIO_write(bd, buf, n) < 0, "%s", dest_name);
	}
	BIO_free(bm);
	ERR(n < 0, "%s", module_name);
	module_size = BIO_number_written(bd);

	if (!raw_sig) {
		ERR(i2d_CMS_bio_stream(bd, cms, NULL, 0) != 1, "%s", dest_name);
	} else {
		BIO *b;

		/* Read the raw signature file and write the data to the
		 * destination file
		 */
		b = BIO_new_file(raw_sig_name, "rb");
		ERR(!b, "%s", raw_sig_name);
		while ((n = BIO_read(b, buf, sizeof(buf))), n > 0)
			ERR(BIO_write(bd, buf, n) < 0, "%s", dest_name);
		BIO_free(b);
	}

	sig_size = BIO_number_written(bd) - module_size;
	sig_info.sig_len = htonl(sig_size);
	ERR(BIO_write(bd, &sig_info, sizeof(sig_info)) < 0, "%s", dest_name);
	ERR(BIO_write(bd, MODULE_SIGNATURE_MARKER, sizeof(MODULE_SIGNATURE_MARKER) - 1) < 0,
	    "%s", dest_name);

	ERR(BIO_free(bd) != 1, "%s", dest_name);

	/* Finally, if we're signing in place, replace the original. */
	if (replace_orig)
		ERR(rename(dest_name, module_name) < 0, "%s", dest_name);

	return 0;
}
