Viewing: sk_utils.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2015, Trustees of Indiana University
 *
 * Copyright (c) 2016, 2017, Intel Corporation.
 *
 * Author: Jeremy Filizetti <jfilizet@iu.edu>
 */

#include <fcntl.h>
#include <limits.h>
#include <math.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <openssl/dh.h>
#include <openssl/err.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
#ifdef HAVE_OPENSSL_EVP_PKEY
#include <openssl/param_build.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include <libcfs/util/string.h>
#include <sys/time.h>
#include <signal.h>
#include <linux/lustre/lgss.h>

#include <lustre/lustreapi.h>
#include "sk_utils.h"
#include "write_bytes.h"

#define SK_PBKDF2_ITERATIONS 10000

#ifdef _NEW_BUILD_
# include "lgss_utils.h"
#else
# include "gss_util.h"
# include "gss_oids.h"
# include "err_util.h"
#endif

int fips_mode = -1;

#ifdef _ERR_UTIL_H_
/**
 * Initializes logging
 * \param[in]	program		Program name to output
 * \param[in]	verbose		Verbose flag
 * \param[in]	fg		Whether or not to run in foreground
 *
 */
void sk_init_logging(char *program, int verbose, int fg)
{
	initerr(program, verbose, fg);
}
#endif

#if !defined(HAVE_OPENSSL_EVP_PKEY) && OPENSSL_VERSION_NUMBER >= 0x10100000L
static int __fetch_ssk_prime(struct sk_keyfile_config *config)
{
	const BIGNUM *p;
	DH *dh = NULL;
	int primenid;
	int rc = -1;

	primenid = sk_primebits2primenid(config->skc_prime_bits);
	dh = DH_new_by_nid(primenid);
	if (!dh) {
		fprintf(stderr, "error: dh cannot be init\n");
		goto prime_end;
	}

	p = DH_get0_p(dh);
	if (!p) {
		fprintf(stderr, "error: cannot get p from dh\n");
		goto prime_end;
	}

	if (BN_num_bytes(p) > SK_MAX_P_BYTES) {
		fprintf(stderr,
			"error: requested length %d exceeds maximum %d\n",
			BN_num_bytes(p), SK_MAX_P_BYTES * 8);
		goto prime_end;
	}

	if (BN_bn2bin(p, config->skc_p) != BN_num_bytes(p)) {
		fprintf(stderr, "error: convert BIGNUM p to binary failed\n");
		goto prime_end;
	}

	rc = 0;

prime_end:
	if (rc)
		fprintf(stderr,
			"error: fetching SSK prime failed: %s\n",
			ERR_error_string(ERR_get_error(), NULL));
	DH_free(dh);
	return rc;
}
#endif

/**
 * Generates the prime required by the client key, and stores it in the
 * struct sk_keyfile_config.
 *
 * \param[in]	config		config describing the key
 *
 * \return	0		success
 * \return	-1		failure
 */
int gen_ssk_prime(struct sk_keyfile_config *config)
{
	int rc = -1;
	const char *primename;
	EVP_PKEY_CTX *ctx = NULL;
	EVP_PKEY *dh = NULL;
	BIGNUM *p = NULL;

	if (fips_mode < 0)
		fips_mode = FIPS_mode();
	if (fips_mode) {
		primename = sk_primebits2name(config->skc_prime_bits);
		if (!primename) {
			fprintf(stderr,
				"error: prime len %d not supported in FIPS mode\n",
				config->skc_prime_bits);
			return rc;
		}
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
		fprintf(stdout,
			"FIPS mode, using well-known prime %s\n", primename);
#ifndef HAVE_OPENSSL_EVP_PKEY
		return __fetch_ssk_prime(config);
#endif
#endif /* OPENSSL_VERSION_NUMBER >= 0x10100000L */
	}

	ctx = EVP_PKEY_CTX_new_from_name(NULL, "DH", NULL);
	if (!ctx || EVP_PKEY_paramgen_init(ctx) != 1) {
		fprintf(stderr, "error: ctx cannot be init\n");
		goto prime_end;
	}

	if (EVP_PKEY_CTX_set_dh_paramgen_prime_len(ctx,
						config->skc_prime_bits) <= 0 ||
	    EVP_PKEY_CTX_set_dh_paramgen_generator(ctx, SK_GENERATOR) <= 0) {
		fprintf(stderr, "error: cannot set prime or generator\n");
		goto prime_end;
	}

	if (EVP_PKEY_paramgen(ctx, &dh) != 1) {
		fprintf(stderr, "error: cannot generate DH parameters\n");
		goto prime_end;
	}

	if (!EVP_PKEY_get_bn_param(dh, OSSL_PKEY_PARAM_FFC_P, &p)) {
		fprintf(stderr, "error: cannot get p from dh\n");
		goto prime_end;
	}

	if (BN_num_bytes(p) > SK_MAX_P_BYTES) {
		fprintf(stderr,
			"error: cannot generate DH parameters: requested length %d exceeds maximum %d\n",
			config->skc_prime_bits, SK_MAX_P_BYTES * 8);
		goto prime_end;
	}
	if (BN_bn2bin(p, config->skc_p) != BN_num_bytes(p)) {
		fprintf(stderr,
			"error: convert BIGNUM p to binary failed\n");
		goto prime_end;
	}

	rc = 0;

prime_end:
	if (rc)
		fprintf(stderr,
			"error: generating SSK prime failed: %s\n",
			ERR_error_string(ERR_get_error(), NULL));
	EVP_PKEY_free(dh);
	EVP_PKEY_CTX_free(ctx);
	return rc;
}

static int write_data_with_error_handling(int fd, const void *data,
					  size_t expected_size,
					  const char *output_file,
					  const char *data_type)
{
	ssize_t rc;
	int retry = 10;

retry_write:
	rc = write(fd, data, expected_size);
	if (rc < 0) {
		fprintf(stderr, "error: writing %zu %s bytes to '%s': %s\n",
			expected_size, data_type, output_file, strerror(errno));
		return -errno;
	} else if (rc != expected_size) {
		/* short write: advance past the bytes written and retry */
		if (--retry > 0) {
			data += rc;
			expected_size -= rc;
			goto retry_write;
		}
		fprintf(stderr,
			"error: short write of %s to '%s' (%zd/%zu bytes)\n",
			data_type, output_file, rc, expected_size);
		return -ENOSPC;
	}

	return 0;
}

/**
 * Writes sk config to file.
 *
 * \param[in]	output_file		output file to write sk config to
 * \param[in]	config			the sk config to write
 * \param[in]	overwrite		true to overwrite existing output file
 * \param[in]	ascii_format		true to write ASCII-encoded output
 *
 * \return	0		success
 * \return	-errno		on failure
 */
int write_config_file(char *output_file, struct sk_keyfile_config *config,
		      bool overwrite, bool ascii_format)
{
	int flags = O_WRONLY | O_CREAT;
	char *ascii_data = NULL;
	size_t ascii_len = 0;
	int fd, rc;

	if (!overwrite)
		flags |= O_EXCL;
	else
		flags |= O_TRUNC | O_SYNC;

	sk_config_cpu_to_disk(config);

	fd = open(output_file, flags, 0400);
	if (fd < 0) {
		fprintf(stderr, "error: opening '%s': %s\n", output_file,
			strerror(errno));
		return -errno;
	}

	if (ascii_format) {
		/* Generate ASCII-encoded output */
		rc = sk_encode_ascii_key(config, &ascii_data, &ascii_len);
		if (rc) {
			fprintf(stderr,
				"error: failed to encode key in ASCII format\n");
			close(fd);
			return -EINVAL;
		}

		rc = write_data_with_error_handling(fd, ascii_data, ascii_len,
						    output_file, "ASCII data");
		free(ascii_data);
	} else {
		/* Binary format output */
		rc = write_data_with_error_handling(fd, config, sizeof(*config),
						    output_file, "data");
	}

	close(fd);
	return rc;
}

/**
 * Loads the key from \a filename and returns the struct sk_keyfile_config.
 * It should be freed by the caller.
 *
 * \param[in]	filename		Disk or key payload data
 *
 * \return	sk_keyfile_config	sucess
 * \return	NULL			failure
 */
struct sk_keyfile_config *sk_read_file(char *filename)
{
	struct sk_keyfile_config *config = NULL;
	char *file_data = NULL;
	char *ptr;
	size_t bytes_read = 0;
	size_t max_size;
	struct stat st;
	int fd;
	ssize_t rc;

	/* allow standard input override */
	if (strcmp(filename, "-") == 0)
		fd = STDIN_FILENO;
	else
		fd = open(filename, O_RDONLY);

	if (fd == -1) {
		printerr(0, "Error opening key file '%s': %s\n", filename,
			 strerror(errno));
		return NULL;
	}

	/* Check file permissions for regular files */
	if (fd != STDIN_FILENO) {
		rc = fstat(fd, &st);
		if (rc == 0 && (st.st_mode & ~(S_IFREG | 0600))) {
			fprintf(stderr, "warning: "
				"secret key '%s' has insecure file mode %#o\n",
				filename, st.st_mode);
		}
	}

	/* Allocate fixed buffer - twice config size should be enough */
	max_size = 2 * sizeof(struct sk_keyfile_config);
	file_data = malloc(max_size + 1);
	if (!file_data) {
		printerr(0, "Failed to allocate memory for file data\n");
		goto out_close;
	}

	/* Read file with size limit */
	ptr = file_data;
	while (bytes_read < max_size) {
		rc = read(fd, ptr, max_size - bytes_read);
		if (rc == -1) {
			if (errno == EINTR)
				continue;
			printerr(0, "read() failed on %s: %s\n", filename,
				 strerror(errno));
			goto out_free;
		} else if (rc == 0) {
			break;
		}

		ptr += rc;
		bytes_read += rc;
	}

	/* Check if file is too large */
	if (bytes_read >= max_size) {
		printerr(0,
			 "File %s too large, exceeds maximum expected size\n",
			 filename);
		goto out_free;
	}

	if (fd != STDIN_FILENO)
		close(fd);
	fd = -1;

	/* Null-terminate for ASCII processing */
	file_data[bytes_read] = '\0';

	if (sk_is_ascii_encoded(file_data, bytes_read)) {
		config = sk_decode_ascii_key(file_data, bytes_read);

		/* Free original buffer since decode allocates new one */
		free(file_data);
	} else {
		/* Binary format - check size and process */
		if (bytes_read != sizeof(struct sk_keyfile_config)) {
			printerr(0,
				"File %s does not have a complete key: got %zu bytes, expected %zu bytes\n",
				filename, bytes_read,
				sizeof(struct sk_keyfile_config));
			goto out_free;
		}

		/* Use the existing buffer as config */
		config = (struct sk_keyfile_config *)file_data;
	}

	if (config)
		sk_config_disk_to_cpu(config);

	return config;

out_free:
	free(file_data);
out_close:
	if (fd != -1)
		close(fd);
	return NULL;
}

/**
 * Build an array of all keys in the user keyring that start with
 * the given description.
 *
 * \param[in]	description   description to match keys against
 * \param[out]	keys_array    found array of keys, last element is a zero key
 *
 * \retval	0		success
 * \retval	-errno		on failure
 */
static int build_keys_array(char *description, key_serial_t **keys_array)
{
	key_serial_t *tmp_keys, *curr_key;
	key_serial_t *keys_array_;
	ssize_t user_kr_len;
	int i;

	if (!keys_array)
		return -EINVAL;

	user_kr_len = keyctl_read_alloc(KEY_SPEC_USER_KEYRING,
					(void **)&tmp_keys);
	if (user_kr_len < 0) {
		printerr(0, "Cannot find any key in user keyring\n");
		*keys_array = NULL;
		return -ENOENT;
	}

	keys_array_ = malloc((user_kr_len / sizeof(key_serial_t) + 1) *
			    sizeof(key_serial_t));
	if (!keys_array_) {
		free(tmp_keys);
		*keys_array = NULL;
		return -ENOMEM;
	}

	curr_key = keys_array_;
	for (i = 0; i < user_kr_len / sizeof(key_serial_t); i++) {
		key_serial_t k = tmp_keys[i];
		char *desc = NULL, *sep;
		ssize_t len;

		len = keyctl_describe_alloc(k, &desc);
		if (len < 0)
			/* ignore keys that cannot be read */
			continue;

		/* The whole description string looks like:
		 * "type;uid;gid;perm;description"
		 * We are only interested in the final "description".
		 */
		sep = strstr(desc, description);
		if (!sep || sep == desc || *(sep - 1) != ';')
			goto next;
		if (sep[strlen(description)] != '\0' &&
		    sep[strlen(description)] != ':')
			goto next;

		*curr_key = k;
		curr_key++;
next:
		free(desc);
	}
	/* Finish the array with a zero key */
	*curr_key = 0;

	free(tmp_keys);

	*keys_array = keys_array_;

	return 0;
}

/*
 * Link @u into the caller's session keyring so we possess the keys we add to
 * @u. keyctl_setperm() here (and keyctl_read()/keyctl_revoke() in
 * sk_remove_key()) require a permission the kernel grants on a newly added key
 * only via possession, not by UID match. @u is linked into @s by pam_keyinit
 * at interactive login, but not under sudo, systemd, cron, or Pacemaker - so
 * without this the setperm silently fails and the key keeps restrictive perms.
 */
static void sk_link_user_keyring(void)
{
	/* EEXIST just means @u was already linked (e.g. interactive login). */
	if (keyctl_link(KEY_SPEC_USER_KEYRING,
			KEY_SPEC_SESSION_KEYRING) < 0 && errno != EEXIST)
		printerr(0, "Failed to link @u to @s: %s\n", strerror(errno));
}

/**
 * Checks if a key matching \a description is found in the keyring for
 * logging purposes and then attempts to load the payload from \a skc keyfile
 * config into a key with \a description.
 *
 * \param[in]	skc		keyfile config to load
 * \param[in]	description	Description used for key in keyring
 * \param[in]	suffix		true to avoid overwriting existing server key,
 *				by adding a suffix to the key desc
 * \param[in]	timeout		timeout in seconds to apply on already existing
 *				keys with same desc
 *
 * \return	>= 0	key serial of key successfully loaded
 * \return	-1	failure
 */
static key_serial_t sk_load_key(const struct sk_keyfile_config *skc,
				char *description, bool suffix, int timeout)
{
	key_serial_t *keys = NULL, *key_p;
	struct sk_keyfile_config payload;
	key_serial_t key;

	memcpy(&payload, skc, sizeof(*skc));

	/* In the keyring use the disk layout so keyctl pipe can be used */
	sk_config_cpu_to_disk(&payload);

	/* Possess keys in @u so the keyctl_setperm() below - and the
	 * keyctl_set_timeout() on former keys - succeed; see
	 * sk_link_user_keyring().
	 */
	sk_link_user_keyring();

	/* Check to see if a key is already loaded matching description */
	key = keyctl_search(KEY_SPEC_USER_KEYRING, "user", description, 0);
	if (key != -1) {
		if (suffix) {
			/* append timestamp to key desc,
			 * SK_DESCRIPTION_SIZE has room for it.
			 */
			size_t desclen = strlen(description);
			struct timeval tv;
			struct tm *tm;
			int rc;

			/* get list of former keys */
			rc = build_keys_array(description, &keys);
			if (rc) {
				printerr(0,
					 "Failed to build keys array: rc=%d\n",
					 rc);
				free(keys);
				return -1;
			}

			gettimeofday(&tv, NULL);
			tm = localtime(&tv.tv_sec);

			/* timestamp expressed as yyyymmdd_HHMMSS_USECS */
			if (snprintf((char *)description + desclen,
				     SK_DESCRIPTION_SIZE - desclen,
				     ":%04d%02d%02d_%02d%02d%02d_%06ld",
				     tm->tm_year + 1900, tm->tm_mon + 1,
				     tm->tm_mday, tm->tm_hour, tm->tm_min,
				     tm->tm_sec, tv.tv_usec) >=
			    SK_DESCRIPTION_SIZE - desclen) {
				free(keys);
				return -1;
			}

			printerr(2,
				 "Key %d found in user keyring, inserting new key with desc %s\n",
				 key, description);
		} else {
			printerr(2,
				 "Key %d found in user keyring, replacing\n",
				 key);
		}
	}

	key = add_key("user", description, &payload, sizeof(payload),
		      KEY_SPEC_USER_KEYRING);
	if (key != -1) {
		key_perm_t perm = KEY_POS_ALL | KEY_USR_ALL |
			KEY_GRP_ALL | KEY_OTH_ALL;

		if (keyctl_setperm(key, perm) < 0)
			printerr(0,
				 "Failed to set perm 0x%x on key %d: %s\n",
				 perm, key, strerror(errno));
		printerr(2, "Added key %d with description %s\n", key,
			 description);

		/* expire former keys to some time in the future */
		key_p = keys;
		while (key_p && *key_p) {
			if (timeout == -1)
				timeout = 2 * 24 * 60 * 60; /* 2 days */

			if (keyctl_set_timeout(*key_p, timeout) < 0)
				printerr(2,
					 "Failed to set timeout %ds on key %d\n",
					 timeout, *key_p);
			else
				printerr(2,
					 "Setting timeout %ds on former key %d\n",
					 timeout, *key_p);
			key_p++;
		}
	} else {
		printerr(0, "Failed to add key with %s\n", description);
	}

	free(keys);
	return key;
}

/**
 * Reads the key from \a path, verifies it and loads into the session keyring
 * using a description determined by the the \a type.  Existing keys with the
 * same description are replaced.
 *
 * \param[in]	path		Path to key file
 * \param[in]	client		Client is mounting with a server key
 * \param[in]	randomize	true to randomize key desc, only apply to client
 * \param[in]	mntdir		Client mount dir
 * \param[in]	suffix		true to avoid overwriting existing server key,
 *				by adding a suffix to the key desc
 * \param[in]	timeout		timeout in seconds to apply on already existing
 *				keys with same desc
 *
 * \return	> 0	client file system key id if successfully loaded
 * \return	  0	other key type successfully loaded
 * \return	< 0	-errno on failure
 */
int sk_load_keyfile(char *path, bool client, bool randomize, char *mntdir,
		    bool suffix, int timeout)
{
	struct sk_keyfile_config *config;
	char description[SK_DESCRIPTION_SIZE + 1];
	struct stat buf;
	int keyid = 0;
	int i, rc;

	rc = stat(path, &buf);
	if (rc == -1) {
		printerr(0, "stat() failed for file %s: %s\n", path,
			 strerror(errno));
		return -errno;
	}

read_sk:
	config = sk_read_file(path);
	if (!config)
		return -ENOKEY;

	/* Similar to ssh, require adequate care of key files */
	if (buf.st_mode & (S_IRGRP | S_IWGRP | S_IWOTH | S_IXOTH)) {
		printerr(0,
			 "Shared key files must be readable/writeable only by owner\n");
		return -EACCES;
	}

	if (sk_validate_config(config)) {
		rc = -ENOKEY;
		goto out;
	}

	/* The server side can have multiple key files per file system so
	 * the nodemap name is appended to the key description to uniquely
	 * identify it */
	if (config->skc_type & SK_TYPE_MGS) {
		/* Any key can be an MGS key as long as we are told to use it */
		rc = snprintf(description, sizeof(description), "lustre:MGS:%s",
			      config->skc_nodemap);
		if (rc >= sizeof(description)) {
			rc = -ENAMETOOLONG;
			goto out;
		}
		if (sk_load_key(config, description, suffix, timeout) == -1) {
			rc = -ENOKEY;
			goto out;
		}
	}
	if (config->skc_type & SK_TYPE_SERVER) {
		if (client) {
			/* Client is mounting with a server key:
			 * generate prime on the fly and reload.
			 */
			config->skc_type = SK_TYPE_CLIENT;

			/* This message is verified by sanity-sec.sh test_100 */
			printf("Generating DH parameters to turn %s into a client key, this can take a while...\n",
				path);
			if (gen_ssk_prime(config))
				goto out;
			if (write_config_file(path, config, true, false))
				goto out;
			free(config);
			goto read_sk;
		}

		/* Server keys need to have the file system name in the key */
		if (config->skc_fsname[0] == '\0') {
			printerr(0,
				 "Key configuration has no file system attribute.  Can't load as server type\n");
			rc = -ENOKEY;
			goto out;
		}
		rc = snprintf(description, sizeof(description), "lustre:%s:%s",
			      config->skc_fsname, config->skc_nodemap);
		if (rc >= sizeof(description)) {
			rc = -ENAMETOOLONG;
			goto out;
		}
		if (sk_load_key(config, description, suffix, timeout) == -1) {
			rc = -ENOKEY;
			goto out;
		}
	}
	if (config->skc_type & SK_TYPE_CLIENT) {
		/* Load client file system key */
		if (config->skc_fsname[0] != '\0') {
			char uuid[UUID_MAX + 2] = { 0 }; /* additional '\n' */
			char rand[5];

			if (mntdir) {
				char fsname[LUSTRE_MAXFSNAME + 1];

				/* If mount path is provided, check it refers to
				 * the same fs as the key.
				 */
				rc = llapi_get_fsname(mntdir, fsname,
						      sizeof(fsname));
				if (rc < 0) {
					printerr(0,
						 "Cannot get Lustre fsname for %s: rc=%d\n",
						 mntdir, rc);
					goto out;
				}
				if (strcmp(fsname, config->skc_fsname) != 0) {
					rc = -EINVAL;
					printerr(0,
						 "Key to load is for file system %s, but %s is mounted at %s: rc=%d\n",
						 config->skc_fsname, fsname,
						 mntdir, rc);
					goto out;
				}

				/* Fetch uuid from mount point */
				rc = llapi_get_agent_uuid(mntdir, uuid,
							  sizeof(uuid));
				if (rc < 0) {
					printerr(0,
						 "Cannot get client uuid for %s: rc=%d\n",
						 mntdir, rc);
					goto out;
				}
				if (uuid[strlen(uuid) - 1] == '\n')
					uuid[strlen(uuid) - 1] = '\0';
			}

			/* add random chars at end of key if requested... */
			if (randomize)
				snprintf(rand, sizeof(rand), "%.4lX", random());
			rc = snprintf(description, sizeof(description),
				      "lustre:%s%s%s", config->skc_fsname,
				      randomize || mntdir ? ":" : "",
				      randomize ? rand : (mntdir ? uuid : ""));
			if (rc >= sizeof(description)) {
				rc = -ENAMETOOLONG;
				goto out;
			}
			keyid = sk_load_key(config, description, false, -1);
			if (keyid == -1) {
				rc = -ENOKEY;
				goto out;
			}

			/* ... and also load legacy key desc for interop */
			if (randomize) {
				rc = snprintf(description, sizeof(description),
					      "lustre:%s", config->skc_fsname);
				if (rc < sizeof(description))
					(void)sk_load_key(config, description,
							  false, -1);
			}
		}

		/* Load client MGC keys */
		for (i = 0; i < MAX_MGSNIDS; i++) {
			if (config->skc_mgsnids[i] == LNET_NID_ANY)
				continue;
			rc = snprintf(description, sizeof(description),
				      "lustre:MGC%s",
				      libcfs_nid2str(config->skc_mgsnids[i]));
			if (rc >= sizeof(description)) {
				rc = -ENAMETOOLONG;
				goto out;
			}
			if (sk_load_key(config, description, false, -1) == -1) {
				rc = -ENOKEY;
				goto out;
			}
		}
	}
	rc = 0;

out:
	free(config);
	if (keyid > 0)
		return keyid;
	return rc;
}

/**
 * Checks if keys matching \a description are found in the keyring. Each key is
 * verified against the key from \a skc, and if it matches, the key is revoked
 * and removed from the keyring.
 *
 * \param[in]	skc		keyfile config to load
 * \param[in]	description	Description used for key in keyring
 *
 * \retval	0		success (at least one key removed)
 * \retval	< 0		failure (no key removed)
 */
static int sk_remove_key(const struct sk_keyfile_config *skc, char *description)
{
	key_serial_t *keys = NULL;
	struct sk_keyfile_config file_payload;
	struct sk_keyfile_config *keyring_payload;
	size_t key_bytes;
	int removed_cnt = 0;
	int i, payload_len, rc;

	memcpy(&file_payload, skc, sizeof(*skc));
	/* store key length in bytes for later key comparison before swab */
	key_bytes = file_payload.skc_shared_keylen / 8;

	/* In the keyring use the disk layout so keyctl pipe can be used */
	sk_config_cpu_to_disk(&file_payload);

	/* Possess keys in @u so the keyctl_read()/keyctl_revoke() below can act
	 * on keys that were loaded with the restrictive default perms (e.g. by
	 * an older mount.lustre, or before this fix); see
	 * sk_link_user_keyring().
	 */
	sk_link_user_keyring();

	rc = build_keys_array(description, &keys);
	if (rc)
		return rc;

	/* for each key on the keyring, sanity check first before unlink:
	 * 1. skc_shared_keylen matches for the file key and keyring key
	 * 2. skc_shared_key matches for the file key and keyring key
	 */
	for (i = 0; keys[i] != 0; i++) {
		payload_len = keyctl_read_alloc(keys[i], (void **)&keyring_payload);
		if (payload_len < 0) {
			printerr(0, "failed to read key %d - skipping\n", keys[i]);
			continue;
		}
		if (payload_len != sizeof(struct sk_keyfile_config)) {
			printerr(0, "key %d has unexpected size %d - skipping\n",
				keys[i], payload_len);
			goto next;
		}

		if (keyring_payload->skc_shared_keylen !=
		    file_payload.skc_shared_keylen) {
			printerr(1,
				"key %d has different shared key length - skipping\n",
				keys[i]);
			goto next;
		}

		if (memcmp(keyring_payload->skc_shared_key,
			   file_payload.skc_shared_key, key_bytes) != 0) {
			printerr(1,
				 "key %d has different shared key - skipping\n",
				 keys[i]);
			goto next;
		}

		rc = keyctl_revoke(keys[i]);
		if (rc == -1) {
			printerr(0, "failed to revoke key %d: %s\n", keys[i],
				 strerror(errno));
			goto next;
		}
		rc = keyctl_unlink(keys[i], KEY_SPEC_USER_KEYRING);
		if (rc == -1) {
			printerr(0, "failed to unlink key %d: %s\n", keys[i],
				 strerror(errno));
			goto next;
		}
		printerr(2, "Key %d revoked and removed from keyring\n",
			 keys[i]);
		removed_cnt++;

next:
		free(keyring_payload);
	}

	free(keys);

	if (removed_cnt > 0)
		printerr(1, "Removed %d key(s) from keyring\n", removed_cnt);
	else
		printerr(0, "No matching key found to remove\n");

	return removed_cnt > 0 ? 0 : -ENOKEY;
}

/**
 * Removes the key from \a path, verifies it and removes matching keys from the
 * keyring.
 *
 * \param[in]	path		Path to key file
 *
 * \retval	0		success (at least one key removed)
 * \retval	< 0		failure (no key removed)
 */
int sk_remove_keyfile(char *path)
{
	struct sk_keyfile_config *config;
	char description[SK_DESCRIPTION_SIZE + 1] = { 0 };
	struct stat buf;
	int rc;

	rc = stat(path, &buf);
	if (rc == -1) {
		printerr(0, "stat() failed for file %s: %s\n", path,
			 strerror(errno));
		return -errno;
	}

	config = sk_read_file(path);
	if (!config)
		return -ENOKEY;

	rc = sk_validate_config(config);
	if (rc != 0) {
		rc = -EINVAL;
		goto out;
	}

	if (config->skc_type & SK_TYPE_MGS) {
		rc = snprintf(description, sizeof(description), "lustre:MGS:%s",
			      config->skc_nodemap);
	} else if (config->skc_type & SK_TYPE_SERVER) {
		rc = snprintf(description, sizeof(description), "lustre:%s:%s",
			      config->skc_fsname, config->skc_nodemap);
	} else if (config->skc_type & SK_TYPE_CLIENT) {
		rc = snprintf(description, sizeof(description), "lustre:%s",
			      config->skc_fsname);
	} else {
		printerr(0, "Invalid key type\n");
		rc = -EINVAL;
		goto out;
	}

	if (rc >= sizeof(description)) {
		rc = -ENAMETOOLONG;
		goto out;
	}

	rc = sk_remove_key(config, description);
out:
	free(config);
	return rc;
}

/**
 * Byte swaps config from cpu format to disk
 *
 * \param[in,out]	config		sk_keyfile_config to swap
 */
void sk_config_cpu_to_disk(struct sk_keyfile_config *config)
{
	int i;

	if (!config)
		return;

	config->skc_version = htobe32(config->skc_version);
	config->skc_hmac_alg = htobe16(config->skc_hmac_alg);
	config->skc_crypt_alg = htobe16(config->skc_crypt_alg);
	config->skc_expire = htobe32(config->skc_expire);
	config->skc_shared_keylen = htobe32(config->skc_shared_keylen);
	config->skc_prime_bits = htobe32(config->skc_prime_bits);

	for (i = 0; i < MAX_MGSNIDS; i++)
		config->skc_mgsnids[i] = htobe64(config->skc_mgsnids[i]);
}

/**
 * Byte swaps config from disk format to cpu
 *
 * \param[in,out]	config		sk_keyfile_config to swap
 */
void sk_config_disk_to_cpu(struct sk_keyfile_config *config)
{
	int i;

	if (!config)
		return;

	config->skc_version = be32toh(config->skc_version);
	config->skc_hmac_alg = be16toh(config->skc_hmac_alg);
	config->skc_crypt_alg = be16toh(config->skc_crypt_alg);
	config->skc_expire = be32toh(config->skc_expire);
	config->skc_shared_keylen = be32toh(config->skc_shared_keylen);
	config->skc_prime_bits = be32toh(config->skc_prime_bits);

	for (i = 0; i < MAX_MGSNIDS; i++)
		config->skc_mgsnids[i] = be64toh(config->skc_mgsnids[i]);
}

/**
 * Verifies the on key payload format is valid
 *
 * \param[in]	config		sk_keyfile_config
 *
 * \return	-1	failure
 * \return	0	success
 */
int sk_validate_config(const struct sk_keyfile_config *config)
{
	int i;

	if (!config) {
		printerr(0, "Null configuration passed\n");
		return -1;
	}

	if (config->skc_version != SK_CONF_VERSION) {
		printerr(0, "Invalid version\n");
		return -1;
	}

	if (config->skc_hmac_alg == SK_HMAC_INVALID) {
		printerr(0, "Invalid HMAC algorithm\n");
		return -1;
	}

	if (config->skc_crypt_alg == SK_CRYPT_INVALID) {
		printerr(0, "Invalid crypt algorithm\n");
		return -1;
	}

	if (config->skc_expire < 60 || config->skc_expire > INT_MAX) {
		/* Try to limit key expiration to some reasonable minimum and
		 * also prevent values over INT_MAX because there appears
		 * to be a type conversion issue */
		printerr(0, "Invalid expiration time should be between %d "
			 "and %d\n", 60, INT_MAX);
		return -1;
	}
	if (config->skc_prime_bits % 8 != 0 ||
	    config->skc_prime_bits > SK_MAX_P_BYTES * 8) {
		printerr(0, "Invalid session key length must be a multiple of 8"
			 " and less then %d bits\n",
			 SK_MAX_P_BYTES * 8);
		return -1;
	}
	if (config->skc_shared_keylen % 8 != 0 ||
	    config->skc_shared_keylen > SK_MAX_KEYLEN_BYTES * 8){
		printerr(0, "Invalid shared key max length must be a multiple "
			 "of 8 and less then %d bits\n",
			 SK_MAX_KEYLEN_BYTES * 8);
		return -1;
	}

	/* Check for terminating nulls on strings */
	for (i = 0; i < sizeof(config->skc_fsname) &&
	     config->skc_fsname[i] != '\0';  i++)
		; /* empty loop */
	if (i == sizeof(config->skc_fsname)) {
		printerr(0, "File system name not null terminated\n");
		return -1;
	}

	for (i = 0; i < sizeof(config->skc_nodemap) &&
	     config->skc_nodemap[i] != '\0';  i++)
		; /* empty loop */
	if (i == sizeof(config->skc_nodemap)) {
		printerr(0, "Nodemap name not null terminated\n");
		return -1;
	}

	if (config->skc_type == SK_TYPE_INVALID) {
		printerr(0, "Invalid key type\n");
		return -1;
	}

	return 0;
}

/**
 * Checks if the given data is ASCII-encoded SSK key format
 *
 * \param[in]	data	Data to check
 * \param[in]	len	Length of data
 *
 * \return	1	ASCII-encoded format
 * \return	0	binary format
 */
int sk_is_ascii_encoded(const char *data, size_t len)
{
	if (len < SK_ASCII_HEADER_LEN)
		return 0;

	return memcmp(data, SK_ASCII_HEADER, SK_ASCII_HEADER_LEN) == 0;
}

/**
 * Decodes ASCII-encoded SSK key data into sk_keyfile_config structure
 *
 * \param[in,out]	ascii_data	ASCII-encoded key data (may be modified)
 * \param[in]		len		Length of ASCII data
 *
 * \return	sk_keyfile_config	success
 * \return	NULL			failure
 */
struct sk_keyfile_config *sk_decode_ascii_key(char *ascii_data, size_t len)
{
	struct sk_keyfile_config *config;
	const char *encoded_start;
	size_t encoded_len;
	int decoded_len;

	if (!sk_is_ascii_encoded(ascii_data, len)) {
		printerr(0, "Data is not ASCII-encoded SSK key format\n");
		return NULL;
	}

	/* Skip the header string and any whitespace */
	encoded_start = ascii_data + SK_ASCII_HEADER_LEN;
	while (encoded_start < ascii_data + len &&
	       (*encoded_start == ' ' || *encoded_start == '\t' ||
		*encoded_start == '\n' || *encoded_start == '\r'))
		encoded_start++;

	encoded_len = len - (encoded_start - ascii_data);
	if (encoded_len <= 0) {
		printerr(0, "No encoded data found after header string\n");
		return NULL;
	}

	/* Remove trailing whitespace */
	while (encoded_len > 0 &&
	       (encoded_start[encoded_len - 1] == ' ' ||
		encoded_start[encoded_len - 1] == '\t' ||
		encoded_start[encoded_len - 1] == '\n' ||
		encoded_start[encoded_len - 1] == '\r'))
		encoded_len--;

	decoded_len = gss_base64url_decode((char **)&encoded_start,
					   ascii_data,
					   sizeof(struct sk_keyfile_config));
	if (decoded_len != sizeof(struct sk_keyfile_config)) {
		printerr(0,
			"Failed to decode base64url data or incorrect size: got %d bytes, expected %zu bytes\n",
			decoded_len, sizeof(struct sk_keyfile_config));
		return NULL;
	}

	/* Allocate new buffer for the result */
	config = malloc(sizeof(struct sk_keyfile_config));
	if (!config) {
		printerr(0, "Failed to allocate memory for config\n");
		return NULL;
	}

	/* Copy the decoded data to the result buffer */
	memcpy(config, ascii_data, sizeof(struct sk_keyfile_config));

	return config;
}

/**
 * Encodes sk_keyfile_config structure into ASCII format
 *
 * \param[in]	config		Key configuration to encode
 * \param[out]	ascii_data	Allocated ASCII-encoded data (caller must free)
 * \param[out]	ascii_len	Length of ASCII-encoded data
 *
 * \return	0	success
 * \return	-1	failure
 */
int sk_encode_ascii_key(const struct sk_keyfile_config *config,
			char **ascii_data, size_t *ascii_len)
{
	char *output = NULL;
	size_t encoded_len;
	size_t total_len;
	char *ptr;
	int len;
	int rc;

	if (!config || !ascii_data || !ascii_len) {
		printerr(0, "Invalid parameters for ASCII encoding\n");
		return -1;
	}

	/* Calculate total length: header + encoded data + newline + null
	 * terminator
	 */
	encoded_len = BASE64URL_CHARS(sizeof(struct sk_keyfile_config));
	total_len = SK_ASCII_HEADER_LEN + encoded_len + 2;

	output = malloc(total_len);
	if (!output) {
		printerr(0, "Failed to allocate memory for ASCII output\n");
		return -1;
	}

	/* header */
	memcpy(output, SK_ASCII_HEADER, SK_ASCII_HEADER_LEN);

	ptr = output + SK_ASCII_HEADER_LEN;
	len = encoded_len + 1; /* +1 for trailing space padding */
	rc = gss_base64url_encode(&ptr, &len, (const __u8 *)config,
				  sizeof(*config));
	if (rc < 0) {
		printerr(0, "Failed to base64url encode key data\n");
		free(output);
		return -1;
	}

	/* back up pointer to trailing space and bound check for new line */
	ptr--;
	if (ptr < output || ptr > output + total_len - 2) {
		printerr(0,
			 "Invalid pointer position after base64url encoding\n");
		free(output);
		return -1;
	}
	/* add newline, overwrite trailing space */
	*ptr++ = '\n';
	*ptr = '\0';

	*ascii_data = output;
	*ascii_len = strlen(output);

	return 0;
}

/**
 * Hashes \a string and places the hash in \a hash
 * at \a hash
 *
 * \param[in]		string		Null terminated string to hash
 * \param[in]		hash_alg	OpenSSL EVP_MD to use for hash
 * \param[in,out]	hash		gss_buffer_desc to hold the result
 *
 * \return	-1	failure
 * \return	0	success
 */
static int sk_hash_string(const char *string, const EVP_MD *hash_alg,
			  gss_buffer_desc *hash)
{
	EVP_MD_CTX *ctx = EVP_MD_CTX_create();
	size_t len = strlen(string);
	unsigned int hashlen;

	if (!hash->value || hash->length < EVP_MD_size(hash_alg))
		goto out_err;
	if (!EVP_DigestInit_ex(ctx, hash_alg, NULL))
		goto out_err;
	if (!EVP_DigestUpdate(ctx, string, len))
		goto out_err;
	if (!EVP_DigestFinal_ex(ctx, hash->value, &hashlen))
		goto out_err;

	EVP_MD_CTX_destroy(ctx);
	hash->length = hashlen;
	return 0;

out_err:
	EVP_MD_CTX_destroy(ctx);
	return -1;
}

/**
 * Hashes \a string and verifies the resulting hash matches the value
 * in \a current_hash
 *
 * \param[in]		string		Null terminated string to hash
 * \param[in]		hash_alg	OpenSSL EVP_MD to use for hash
 * \param[in,out]	current_hash	gss_buffer_desc to compare to
 *
 * \return	gss error	failure
 * \return	GSS_S_COMPLETE	success
 */
uint32_t sk_verify_hash(const char *string, const EVP_MD *hash_alg,
			const gss_buffer_desc *current_hash)
{
	gss_buffer_desc hash;
	unsigned char hashbuf[EVP_MAX_MD_SIZE];

	hash.value = hashbuf;
	hash.length = sizeof(hashbuf);

	if (sk_hash_string(string, hash_alg, &hash))
		return GSS_S_FAILURE;
	if (current_hash->length != hash.length)
		return GSS_S_DEFECTIVE_TOKEN;
	if (memcmp(current_hash->value, hash.value, hash.length))
		return GSS_S_BAD_SIG;

	return GSS_S_COMPLETE;
}

static inline int sk_config_has_mgsnid(struct sk_keyfile_config *config,
				       const char *mgsnid)
{
	lnet_nid_t nid;
	int i;

	nid = libcfs_str2nid(mgsnid);
	if (nid == LNET_NID_ANY)
		return 0;

	for (i = 0; i < MAX_MGSNIDS; i++)
		if  (config->skc_mgsnids[i] == nid)
			return 1;
	return 0;
}

/**
 * Create an sk_cred structure populated with initial configuration info and the
 * key. \a tgt \a uuid and \a nodemap are used in determining the expected key
 * description so the key can be found by searching the keyring.
 * This is done because there is no easy way to pass keys from the mount command
 * all the way to the request_key call.  In addition any keys can be dynamically
 * added to the keyrings and still found.  The keyring that needs to be used
 * must be the session keyring.
 * This function can be called repeatedly to cycle through multiple matching
 * keys in the kernel keyring, if non-NULL pointers are provided for
 * \a user_keys_p and \a key_p. The \a key_p pointer is internally updated to
 * point to the key to use. When \a user_keys_p is no longer needed, it must be
 * freed by the caller.
 *
 * \param[in]	tgt		Target file system
 * \param[in]	nodemap		Cluster name for the key.  This correlates to
 *				the nodemap name and is used by the server side.
 *				For the client this will be NULL.
 * \param[in]	uuid		Client uuid
 * \param[in]	flags		Flags for the credentials
 * \param[out]	user_keys_p	Pointer to keys array, must be freed by caller
 * \param[in,out]  key_p	Pointer to current key in array, internal use
 *
 * \return	sk_cred Allocated struct sk_cred on success
 * \return	NULL	failure
 */
struct sk_cred *sk_create_cred(const char *tgt, const char *nodemap,
			       const char *uuid, const uint32_t flags,
			       void **user_keys_p, void **key_p)
{
	char description[SK_DESCRIPTION_SIZE + 1] = { 0 };
	char fsname[MTI_NAME_MAXLEN + 1] = { 0 };
	struct sk_keyfile_config *config;
	struct sk_kernel_ctx *kctx;
	struct sk_cred *skc = NULL;
	const char *mgsnid = NULL;
	long sk_key;
	char *ptr;
	int len, keylen, rc;

	/* extract the file system name from target */
	ptr = index(tgt, '-');
	if (!ptr) {
		len = strlen(tgt);

		/* This must be an MGC target */
		if (strncmp(tgt, "MGC", 3) || len <= 3) {
			printerr(0, "Invalid target name\n");
			return NULL;
		}
		mgsnid = tgt + 3;
	} else {
		len = ptr - tgt;
	}

	if (len > MTI_NAME_MAXLEN) {
		printerr(0, "Invalid target name\n");
		return NULL;
	}
	memcpy(fsname, tgt, len);

	if (key_p && *key_p)
		goto use_key;

	printerr(2, "Creating credentials for target: %s with nodemap: %s\n",
		 tgt, nodemap);

build_desc:
	if (nodemap) {
		if (mgsnid)
			rc = snprintf(description, sizeof(description),
				      "lustre:MGS:%s", nodemap);
		else
			rc = snprintf(description, sizeof(description),
				      "lustre:%s:%s", fsname, nodemap);
	} else {
		rc = snprintf(description, sizeof(description), "lustre:%s%s%s",
			      fsname, uuid ? ":" : "", uuid ?: "");
	}

	if (rc >= sizeof(description)) {
		printerr(0, "Invalid key description\n");
		return NULL;
	}

	if (key_p && user_keys_p && *user_keys_p == NULL) {
		if (*key_p) {
			printerr(0, "Invalid key index\n");
			return NULL;
		}

		rc = build_keys_array(description,
				      (key_serial_t **)user_keys_p);
		if (rc) {
			printerr(0, "Failed to build keys array: rc=%d\n", rc);
			return NULL;
		}
		*key_p = *user_keys_p;
	}

	if (key_p && *key_p) {
use_key:
		sk_key = *((key_serial_t *)*key_p);
		if (!sk_key) {
			printerr(0, "No more valid keys in user keyring\n");
			return NULL;
		}
		printerr(2, "Using key with serial 0x%lx\n", sk_key);
	} else {
		printerr(2, "Searching for key with description: %s\n",
			 description);
		sk_key = keyctl_search(KEY_SPEC_USER_KEYRING, "user",
				       description, 0);
		if (sk_key == -1) {
			if (!nodemap && uuid) {
				uuid = NULL;
				goto build_desc;
			}
			printerr(1, "No key found for %s\n", description);
			return NULL;
		}
	}

	keylen = keyctl_read_alloc(sk_key, (void **)&config);
	if (keylen == -1) {
		printerr(0, "keyctl_read() failed for key %ld: %s\n", sk_key,
			 strerror(errno));
		return NULL;
	} else if (keylen != sizeof(*config)) {
		printerr(0, "Unexpected key size: %d returned for key %ld, "
			 "expected %zu bytes\n",
			 keylen, sk_key, sizeof(*config));
		goto out_err;
	}

	sk_config_disk_to_cpu(config);

	if (sk_validate_config(config)) {
		printerr(0, "Invalid key configuration for key: %ld\n", sk_key);
		goto out_err;
	}

	if (mgsnid && !sk_config_has_mgsnid(config, mgsnid)) {
		printerr(0, "Target name does not match key's MGS NIDs\n");
		goto out_err;
	}

	if (!mgsnid && strcmp(fsname, config->skc_fsname)) {
		printerr(0, "Target name does not match key's file system\n");
		goto out_err;
	}

	skc = malloc(sizeof(*skc));
	if (!skc) {
		printerr(0, "Failed to allocate memory for sk_cred\n");
		goto out_err;
	}

	/* this initializes all gss_buffer_desc to empty as well */
	memset(skc, 0, sizeof(*skc));

	skc->sc_flags = flags;
	skc->sc_tgt.length = strlen(tgt) + 1;
	skc->sc_tgt.value = malloc(skc->sc_tgt.length);
	if (!skc->sc_tgt.value) {
		printerr(0, "Failed to allocate memory for target\n");
		goto out_err;
	}
	memcpy(skc->sc_tgt.value, tgt, skc->sc_tgt.length);

	skc->sc_nodemap_hash.length = EVP_MD_size(EVP_sha256());
	skc->sc_nodemap_hash.value = malloc(skc->sc_nodemap_hash.length);
	if (!skc->sc_nodemap_hash.value) {
		printerr(0, "Failed to allocate memory for nodemap hash\n");
		goto out_err;
	}

	if (sk_hash_string(config->skc_nodemap, EVP_sha256(),
			   &skc->sc_nodemap_hash)) {
		printerr(0, "Failed to generate hash for nodemap name\n");
		goto out_err;
	}

	kctx = &skc->sc_kctx;
	kctx->skc_version = config->skc_version;
	strcpy(kctx->skc_hmac_alg, sk_hmac2name(config->skc_hmac_alg));
	strcpy(kctx->skc_crypt_alg, sk_crypt2name(config->skc_crypt_alg));
	kctx->skc_expire = config->skc_expire;

	/* key payload format is in bits, convert to bytes */
	kctx->skc_shared_key.length = config->skc_shared_keylen / 8;
	kctx->skc_shared_key.value = malloc(kctx->skc_shared_key.length);
	if (!kctx->skc_shared_key.value) {
		printerr(0, "Failed to allocate memory for shared key\n");
		goto out_err;
	}
	memcpy(kctx->skc_shared_key.value, config->skc_shared_key,
	       kctx->skc_shared_key.length);

	skc->sc_p.length = config->skc_prime_bits / 8;
	skc->sc_p.value = malloc(skc->sc_p.length);
	if (!skc->sc_p.value) {
		printerr(0, "Failed to allocate p\n");
		goto out_err;
	}
	memcpy(skc->sc_p.value, config->skc_p, skc->sc_p.length);

	free(config);

	if (key_p && *key_p)
		/* update key pointer to next in array */
		*key_p = ((key_serial_t *)*key_p) + 1;

	return skc;

out_err:
	sk_free_cred(skc);

	free(config);
	return NULL;
}

#define SK_GENERATOR 2
#define DH_NUMBER_ITERATIONS_FOR_PRIME 64

/* OpenSSL 1.1.1c increased the number of rounds used for Miller-Rabin testing
 * of the prime provided as input parameter to DH_check(). This makes the check
 * roughly x10 longer, and causes request timeouts when an SSK flavor is being
 * used.
 * Instead, use a dynamic number Miller-Rabin rounds based on the speed of the
 * check on the current system, evaluated when the lsvcgssd daemon starts, but
 * at least as many as OpenSSL 1.1.1b used for the same key size. If default
 * DH_check() duration is OK, use it directly instead of limiting the rounds.
 * If \a num_rounds == 0, we just call original DH_check() directly.
 *
 * OpenSSL v3 internally forces a minimum of 64 rounds when checking prime, so
 * it is no longer possible to test prime check speed with fewer rounds. In this
 * case, do not bother and directly call EVP_PKEY_param_check.
 */
#ifdef HAVE_OPENSSL_EVP_PKEY
static bool sk_is_dh_valid(EVP_PKEY_CTX *ctx)
{
	if (EVP_PKEY_param_check(ctx) != 1) {
		printerr(0, "EVP_PKEY_param_check failed\n");
		ERR_print_errors_fp(stderr);
		return false;
	}
	return true;
}
#else
static inline bool sk_check_dh(const DH *dh, int num_rounds, bool fullcheck)
{
	const BIGNUM *p = NULL, *g = NULL;
	BN_ULONG word;
	BN_CTX *ctx;
	BIGNUM *r;
	bool valid = false;
	int rc;

	DH_get0_pqg(dh, &p, NULL, &g);
	if (!p || !g)
		return false;

	if (!BN_is_word(g, SK_GENERATOR)) {
		printerr(0, "%s: Diffie-Hellman generator is not %u\n",
			 program_invocation_short_name, SK_GENERATOR);
		return false;
	}

	word = BN_mod_word(p, 24);
	/* OpenSSL v3 changed the way the prime is generated,
	 * using p mod 24 == 23.
	 * So we must accept word == 23 if the prime was generated
	 * by a client with OpenSSL v3.
	 */
	if ((word != 11) && (word != 23)) {
		printerr(0, "%s: Diffie-Hellman prime modulo=%lu unsuitable\n",
			 program_invocation_short_name, word);
		return false;
	}

	if (!fullcheck)
		return true;

	ctx = BN_CTX_new();
	if (ctx == NULL) {
		printerr(0, "%s: Diffie-Hellman error allocating context\n",
			 program_invocation_short_name);
		return false;
	}
	BN_CTX_start(ctx);
	r = BN_CTX_get(ctx); /* must be called before "ctx" used elsewhere */

	rc = BN_is_prime_ex(p, num_rounds, ctx, NULL);
	if (rc == 0)
		printerr(0, "%s: Diffie-Hellman 'p' not prime in %u rounds\n",
			 program_invocation_short_name, num_rounds);
	if (rc <= 0)
		goto out_free;

	if (!BN_rshift1(r, p)) {
		printerr(0, "%s: error shifting BigNum 'r' by 'p'\n",
			 program_invocation_short_name);
		goto out_free;
	}
	rc = BN_is_prime_ex(r, num_rounds, ctx, NULL);
	if (rc == 0)
		printerr(0, "%s: Diffie-Hellman 'r' not prime in %u rounds\n",
			 program_invocation_short_name, num_rounds);
	if (rc <= 0)
		goto out_free;

	valid = true;

out_free:
	BN_CTX_end(ctx);
	BN_CTX_free(ctx);

	return valid;
}

static bool sk_is_dh_valid(const DH *dh, int num_rounds)
{
	int rc;

	if (num_rounds == 0) {
		int codes = 0;

		rc = DH_check(dh, &codes);
		if (codes == DH_NOT_SUITABLE_GENERATOR &&
		    sk_check_dh(dh, num_rounds, false))
			return true;
		if (rc != 1 || codes) {
			printerr(0, "DH_check(0) failed: codes=%#x: rc=%d\n",
				 codes, rc);
			return false;
		}
		return true;
	}

	return sk_check_dh(dh, num_rounds, true);
}
#endif

#ifndef HAVE_OPENSSL_EVP_PKEY
#define VALUE_LENGTH 256
static unsigned char test_prime[VALUE_LENGTH] =
	"\xf7\xfa\x49\xd8\xec\xb1\x3b\xff\x26\x10\x3f\xc5\x3a\xc5\xcc\x40"
	"\x4f\xbf\x92\xe1\x8b\x83\xe7\xa2\xba\x0f\x51\x5a\x91\x48\xe0\xa3"
	"\xf1\x4d\xbc\xbb\x8a\x28\x14\xac\x02\x23\x76\x42\x17\x4d\x3c\xdc"
	"\x5e\x4f\x80\x1f\xd7\x54\x1c\x50\xac\x3b\x28\x68\x8d\x71\x41\x7f"
	"\xa7\x1c\x2f\x22\xd3\xa8\x91\xb2\x64\xb6\x84\xa6\xcf\x06\x16\x91"
	"\x2f\xb8\xb4\x42\x1d\x3a\x4e\x3a\x0c\x7f\x04\x69\x78\xb5\x8f\x92"
	"\x07\x89\xac\x24\x06\x53\x2c\x23\xec\xaa\x5c\xb4\x7b\x49\xbc\xf4"
	"\x90\x67\x71\x9c\x24\x2c\x1d\x8d\x76\xc8\x85\x4e\x19\xf1\xf9\x33"
	"\x45\xbd\x9f\x7d\x0a\x08\x8c\x22\xcc\x35\xf3\x5b\xab\x3f\x24\x9d"
	"\x61\x70\x86\xbb\xbe\xd8\xb0\xf8\x34\xfa\xeb\x5b\x8e\xf2\x62\x23"
	"\xd1\xfb\xbb\xb8\x21\x71\x1e\x39\x39\x59\xe0\x82\x98\x41\x84\x40"
	"\x1f\xd3\x9b\xa3\x73\xdb\xec\xe0\xc0\xde\x2d\x1c\xea\x43\x40\x93"
	"\x98\x38\x03\x36\x1e\xe1\xe7\x39\x7b\x35\x92\x4a\x51\xa5\x91\x63"
	"\xd5\x31\x98\x3d\x89\x27\x6f\xcc\x69\xff\xbe\x31\x13\xdc\x2f\x72"
	"\x2d\xab\x6a\xb7\x13\xd3\x47\xda\xaa\xf3\x3c\xa0\xfd\xaa\x0f\x02"
	"\x96\x81\x1a\x26\xe8\xf7\x25\x65\x33\x78\xd9\x6b\x6d\xb0\xd9\xfb";

/**
 * Measure time taken by prime testing routine for a 2048 bit long prime,
 * depending on the number of check rounds.
 *
 * \param[in]	usec_check_max    max time allowed for DH_check completion
 *
 * \retval	max number of rounds to keep prime testing under usec_check_max
 *		return 0 if we should use the default DH_check rounds
 */
int sk_speedtest_dh_valid(unsigned int usec_check_max, pid_t *child)
{
	DH *dh;
	BIGNUM *p, *g;
	struct sigaction sa;
	int num_rounds, prev_rounds = 0;

	/* Set SIGCHLD disposition so that child that terminates
	 * does not become a zombie.
	 */
	sa.sa_handler = NULL;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_NOCLDWAIT;
	if (sigaction(SIGCHLD, &sa, NULL) == -1)
		printerr(2, "SIGCHLD sigaction failed\n");

	*child = fork();
	if (*child == -1) {
		printerr(0, "cannot fork child for speedtest: %s\n",
			 strerror(errno));
		/* in this case the speedtest cannot be run, so return 0
		 * to use default num rounds for prime testing
		 */
		return 0;
	} else if (*child != 0) {
		/* parent returns immediately,
		 * 0 means num rounds is not determined yet
		 */
		return 0;
	}

	/* now in forked child process, start speed test */

	dh = DH_new();
	if (!dh)
		return 0;

	p = BN_bin2bn(test_prime, VALUE_LENGTH, NULL);
	if (!p)
		goto free_dh;

	g = BN_new();
	if (!g)
		goto free_p;

	if (!BN_set_word(g, SK_GENERATOR))
		goto free_g;

	/* "dh" takes over freeing of 'p' and 'g' if this succeeds */
	if (!DH_set0_pqg(dh, p, NULL, g)) {
	free_g:
		BN_free(g);
	free_p:
		BN_free(p);
		goto free_dh;
	}

	for (num_rounds = 0;
	     num_rounds <= DH_NUMBER_ITERATIONS_FOR_PRIME;
	     num_rounds += (num_rounds <= 4 ? 4 : 8)) {
		unsigned int usec_this;
		int j;

		/* get max duration of 4 runs at current number of rounds */
		usec_this = 0;
		for (j = 0; j < 4; j++) {
			struct timeval now, prev;
			unsigned int usec_curr;

			gettimeofday(&prev, NULL);
			if (!sk_is_dh_valid(dh, num_rounds)) {
				/* if test_prime is found bad, use default */
				prev_rounds = 0;
				goto free_dh;
			}
			gettimeofday(&now, NULL);
			usec_curr = (now.tv_sec - prev.tv_sec) * 1000000 +
				    now.tv_usec - prev.tv_usec;
			if (usec_curr > usec_this)
				usec_this = usec_curr;
		}
		printerr(2, "%s: %d rounds: %d usec\n",
			 program_invocation_short_name, num_rounds, usec_this);
		if (num_rounds == 0) {
			if (usec_this <= usec_check_max)
			/* using original check rounds as implemented in
			 * DH_check() took less time than the max allowed,
			 * so just use original DH_check()
			 */
				break;
		} else if (usec_this >= usec_check_max) {
			break;
		}
		prev_rounds = num_rounds;
	}

free_dh:
	DH_free(dh);

	return prev_rounds;
}
#endif /* !HAVE_OPENSSL_EVP_PKEY */

#ifdef HAVE_OPENSSL_EVP_PKEY
static uint32_t __sk_gen_params(struct sk_cred *skc, BIGNUM *p, BIGNUM *g,
				int num_rounds)
{
	EVP_PKEY_CTX *ctx = NULL, *ctx_from_key = NULL;
	OSSL_PARAM_BLD *tmpl = NULL;
	OSSL_PARAM *params = NULL;
	EVP_PKEY *key = NULL;
	uint32_t rc = GSS_S_FAILURE;

	tmpl = OSSL_PARAM_BLD_new();
	if (!tmpl ||
	    !OSSL_PARAM_BLD_push_BN(tmpl, OSSL_PKEY_PARAM_FFC_P, p) ||
	    !OSSL_PARAM_BLD_push_BN(tmpl, OSSL_PKEY_PARAM_FFC_G, g)) {
		printerr(0, "error: params cannot be pushed\n");
		goto err;
	}
	params = OSSL_PARAM_BLD_to_param(tmpl);
	if (!params) {
		printerr(0, "error: params cannot be allocated\n");
		goto err;
	}

	ctx = EVP_PKEY_CTX_new_from_name(NULL, "DH", NULL);
	if (!ctx ||
	    EVP_PKEY_fromdata_init(ctx) != 1 ||
	    EVP_PKEY_fromdata(ctx, &key,
			      EVP_PKEY_KEY_PARAMETERS, params) != 1) {
		printerr(0, "error: params cannot be set\n");
		goto err;
	}

	ctx_from_key = EVP_PKEY_CTX_new_from_pkey(NULL, key, NULL);
	if (!ctx_from_key) {
		printerr(0, "error: ctx_from_key cannot be allocated\n");
		goto err;
	}

	/* Verify that we have a safe prime and valid generator */
	if (!sk_is_dh_valid(ctx_from_key))
		goto err;

	skc->sc_params = NULL;
	if (EVP_PKEY_keygen_init(ctx_from_key) != 1 ||
	    EVP_PKEY_keygen(ctx_from_key, &skc->sc_params) != 1) {
		printerr(0, "Failed to generate public DH key: %s\n",
			 ERR_error_string(ERR_get_error(), NULL));
		goto err;
	}

	/* skc->sc_pub_key.value is allocated by
	 * EVP_PKEY_get1_encoded_public_key
	 */
	skc->sc_pub_key.length =
	  EVP_PKEY_get1_encoded_public_key(skc->sc_params,
				      (unsigned char **)&skc->sc_pub_key.value);
	if (skc->sc_pub_key.length == 0) {
		printerr(0, "error: cannot get pub key\n");
		skc->sc_pub_key.value = NULL;
		goto err;
	}
	rc = GSS_S_COMPLETE;

err:
	EVP_PKEY_CTX_free(ctx_from_key);
	EVP_PKEY_free(key);
	EVP_PKEY_CTX_free(ctx);
	OSSL_PARAM_free(params);
	OSSL_PARAM_BLD_free(tmpl);
	BN_free(g);
	BN_free(p);
	return rc;
}
#else /* !HAVE_OPENSSL_EVP_PKEY */
static uint32_t __sk_gen_params(struct sk_cred *skc, BIGNUM *p, BIGNUM *g,
				int num_rounds)
{
	const BIGNUM *pub_key;

	/* Populate DH parameters */
	/* "dh" takes over freeing of 'p' and 'g' if this succeeds */
	skc->sc_params = DH_new();
	if (!skc->sc_params || !DH_set0_pqg(skc->sc_params, p, NULL, g)) {
		printerr(0, "Failed to set pqg\n");
		BN_free(g);
		BN_free(p);
		return GSS_S_FAILURE;
	}

	/* Verify that we have a safe prime and valid generator */
	if (!sk_is_dh_valid(skc->sc_params, num_rounds))
		return GSS_S_FAILURE;

	if (DH_generate_key(skc->sc_params) != 1) {
		printerr(0, "Failed to generate public DH key: %s\n",
			 ERR_error_string(ERR_get_error(), NULL));
		return GSS_S_FAILURE;
	}

	DH_get0_key(skc->sc_params, &pub_key, NULL);
	skc->sc_pub_key.length = BN_num_bytes(pub_key);
	skc->sc_pub_key.value = malloc(skc->sc_pub_key.length);
	if (!skc->sc_pub_key.value) {
		printerr(0, "Failed to allocate memory for public key\n");
		return GSS_S_FAILURE;
	}

	BN_bn2bin(pub_key, skc->sc_pub_key.value);

	return GSS_S_COMPLETE;
}
#endif /* HAVE_OPENSSL_EVP_PKEY */

/**
 * Populates the DH parameters for the DHKE
 *
 * \param[in,out]	skc		Shared key credentials structure to
 *					populate with DH parameters
 *
 * \retval	GSS_S_COMPLETE	success
 * \retval	GSS_S_FAILURE	failure
 */
uint32_t sk_gen_params(struct sk_cred *skc, int num_rounds)
{
	uint32_t random;
	BIGNUM *p, *g;

	/* Random value used by both the request and response as part of the
	 * key binding material.  This also should ensure we have unqiue
	 * tokens that are sent to the remote server which is important because
	 * the token is hashed for the sunrpc cache lookups and a failure there
	 * would cause connection attempts to fail indefinitely due to the large
	 * timeout value on the server side.
	 */
	if (RAND_bytes((unsigned char *)&random, sizeof(random)) != 1) {
		printerr(0, "Failed to get data for random parameter: %s\n",
			 ERR_error_string(ERR_get_error(), NULL));
		return GSS_S_FAILURE;
	}

	/* The random value will always be used in byte range operations
	 * so we keep it as big endian from this point on.
	 */
	skc->sc_kctx.skc_host_random = random;

	p = BN_bin2bn(skc->sc_p.value, skc->sc_p.length, NULL);
	if (!p) {
		printerr(0, "Failed to convert binary to BIGNUM\n");
		return GSS_S_FAILURE;
	}

	/* We use a static generator for shared key */
	g = BN_new();
	if (!g) {
		printerr(0, "Failed to allocate new BIGNUM\n");
		goto free_p;
	}
	if (BN_set_word(g, SK_GENERATOR) != 1) {
		printerr(0, "Failed to set g value for DH params\n");
		goto free_g;
	}

	return __sk_gen_params(skc, p, g, num_rounds);

free_g:
	BN_free(g);
free_p:
	BN_free(p);

	return GSS_S_FAILURE;
}

/**
 * Convert SK hash algorithm into openssl message digest
 *
 * \param[in,out]	alg		SK hash algorithm
 *
 * \retval		EVP_MD
 */
static inline const EVP_MD *sk_hash_to_evp_md(enum cfs_crypto_hash_alg alg)
{
	switch (alg) {
	case CFS_HASH_ALG_SHA256:
		return EVP_sha256();
	case CFS_HASH_ALG_SHA512:
		return EVP_sha512();
	default:
		return EVP_md_null();
	}
}

/**
 * Signs (via HMAC) the parameters used only in the key initialization protocol.
 *
 * \param[in]		key		Key to use for HMAC
 * \param[in]		bufs		Array of gss_buffer_desc to generate
 *					HMAC for
 * \param[in]		numbufs		Number of buffers in array
 * \param[in]		hash_alg	OpenSSL EVP_MD to use for hash
 * \param[in,out]	hmac		HMAC of buffers is allocated and placed
 *					in this gss_buffer_desc.  Caller must
 *					free this.
 *
 * \retval	0	success
 * \retval	-1	failure
 */
int sk_sign_bufs(gss_buffer_desc *key, gss_buffer_desc *bufs, const int numbufs,
		 const EVP_MD *hash_alg, gss_buffer_desc *hmac)
{
	unsigned int hashlen = EVP_MD_size(hash_alg);
	EVP_MAC_CTX *ctx = NULL;
	EVP_MAC *mac = NULL;
	size_t len = 0;
	int i, rc = -1;
	DECLARE_EVP_MD(subalg, hash_alg);

	if (hash_alg == EVP_md_null()) {
		printerr(0, "Invalid hash algorithm\n");
		return -1;
	}

	hmac->length = hashlen;
	hmac->value = malloc(hashlen);
	if (!hmac->value) {
		printerr(0, "Failed to allocate memory for HMAC\n");
		goto out;
	}

	mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
	if (!mac) {
		printerr(0, "Failed to fetch HMAC\n");
		goto out;
	}

	ctx = EVP_MAC_CTX_new(mac);
	if (!ctx) {
		printerr(0, "Failed to init HMAC ctx\n");
		goto out;
	}

	if (EVP_MAC_init(ctx, key->value, key->length, subalg) != 1) {
		printerr(0, "Failed to init HMAC\n");
		goto out;
	}

	for (i = 0; i < numbufs; i++) {
		if (EVP_MAC_update(ctx, bufs[i].value, bufs[i].length) != 1) {
			printerr(0, "Failed to update HMAC\n");
			goto out;
		}
	}

	/* The result gets populated in hmac */
	if (EVP_MAC_final(ctx, hmac->value, &len, hashlen) != 1) {
		printerr(0, "Failed to finalize HMAC\n");
		goto out;
	}
	if (hmac->length != len) {
		printerr(0, "HMAC size %zu does not match expected %zu\n",
			 len, hmac->length);
		goto out;
	}

	rc = 0;
out:
	EVP_MAC_CTX_free(ctx);
	EVP_MAC_free(mac);
	return rc;
}

/**
 * Generates an HMAC for gss_buffer_desc array in \a bufs of \a numbufs
 * and verifies against \a hmac.
 *
 * \param[in]	skc		Shared key credentials
 * \param[in]	bufs		Array of gss_buffer_desc to generate HMAC for
 * \param[in]	numbufs		Number of buffers in array
 * \param[in]	hash_alg	OpenSSL EVP_MD to use for hash
 * \param[in]	hmac		HMAC to verify against
 *
 * \retval	GSS_S_COMPLETE	success (match)
 * \retval	gss error	failure
 */
uint32_t sk_verify_hmac(struct sk_cred *skc, gss_buffer_desc *bufs,
			const int numbufs, const EVP_MD *hash_alg,
			gss_buffer_desc *hmac)
{
	gss_buffer_desc bufs_hmac;
	int rc;

	if (sk_sign_bufs(&skc->sc_kctx.skc_shared_key, bufs, numbufs, hash_alg,
			 &bufs_hmac)) {
		printerr(0, "Failed to sign buffers to verify HMAC\n");
		if (bufs_hmac.value)
			free(bufs_hmac.value);
		return GSS_S_FAILURE;
	}

	if (hmac->length != bufs_hmac.length) {
		printerr(0, "Invalid HMAC size\n");
		free(bufs_hmac.value);
		return GSS_S_BAD_SIG;
	}

	rc = memcmp(hmac->value, bufs_hmac.value, bufs_hmac.length);
	free(bufs_hmac.value);

	if (rc)
		return GSS_S_BAD_SIG;

	return GSS_S_COMPLETE;
}

/**
 * Cleanup an sk_cred freeing any resources
 *
 * \param[in,out]	skc	Shared key credentials to free
 */
void sk_free_cred(struct sk_cred *skc)
{
	if (!skc)
		return;

	if (skc->sc_p.value)
		free(skc->sc_p.value);
	if (skc->sc_pub_key.value)
		free(skc->sc_pub_key.value);
	if (skc->sc_tgt.value)
		free(skc->sc_tgt.value);
	if (skc->sc_nodemap_hash.value)
		free(skc->sc_nodemap_hash.value);
	if (skc->sc_hmac.value)
		free(skc->sc_hmac.value);

	/* Overwrite keys and IV before freeing */
	if (skc->sc_dh_shared_key.value) {
		memset(skc->sc_dh_shared_key.value, 0,
		       skc->sc_dh_shared_key.length);
		free(skc->sc_dh_shared_key.value);
	}
	if (skc->sc_kctx.skc_hmac_key.value) {
		memset(skc->sc_kctx.skc_hmac_key.value, 0,
		       skc->sc_kctx.skc_hmac_key.length);
		free(skc->sc_kctx.skc_hmac_key.value);
	}
	if (skc->sc_kctx.skc_encrypt_key.value) {
		memset(skc->sc_kctx.skc_encrypt_key.value, 0,
		       skc->sc_kctx.skc_encrypt_key.length);
		free(skc->sc_kctx.skc_encrypt_key.value);
	}
	if (skc->sc_kctx.skc_shared_key.value) {
		memset(skc->sc_kctx.skc_shared_key.value, 0,
		       skc->sc_kctx.skc_shared_key.length);
		free(skc->sc_kctx.skc_shared_key.value);
	}
	if (skc->sc_kctx.skc_session_key.value) {
		memset(skc->sc_kctx.skc_session_key.value, 0,
		       skc->sc_kctx.skc_session_key.length);
		free(skc->sc_kctx.skc_session_key.value);
	}

	if (skc->sc_params) {
		EVP_PKEY_free(skc->sc_params);
		skc->sc_params = NULL;
	}

	free(skc);
	skc = NULL;
}

/* This function handles key derivation using the hash algorithm specified in
 * \a hash_alg, buffers in \a key_binding_bufs, and original key in
 * \a origin_key to produce a \a derived_key.  The first element of the
 * key_binding_bufs array is reserved for the counter used in the KDF.  The
 * derived key in \a derived_key could differ in size from \a origin_key and
 * must be populated with the expected size and a valid buffer to hold the
 * contents.
 *
 * If the derived key size is greater than the HMAC algorithm size it will be
 * a done using several iterations of a counter and the key binding bufs.
 *
 * If the size is smaller it will take copy the first N bytes necessary to
 * fill the derived key. */
static int sk_kdf(gss_buffer_desc *derived_key, gss_buffer_desc *origin_key,
		  gss_buffer_desc *key_binding_bufs, int numbufs,
		  enum cfs_crypto_hash_alg hmac_alg)
{
	size_t remain;
	size_t bytes;
	uint32_t counter;
	char *keydata;
	gss_buffer_desc tmp_hash;
	int i;
	int rc;

	if (numbufs < 1)
		return -EINVAL;

	/* Use a counter as the first buffer followed by the key binding
	 * buffers in the event we need more than one a single cycle to
	 * produced a symmetric key large enough in size */
	key_binding_bufs[0].value = &counter;
	key_binding_bufs[0].length = sizeof(counter);

	remain = derived_key->length;
	keydata = derived_key->value;
	i = 0;
	while (remain > 0) {
		counter = htobe32(i++);
		rc = sk_sign_bufs(origin_key, key_binding_bufs, numbufs,
				  sk_hash_to_evp_md(hmac_alg), &tmp_hash);
		if (rc) {
			if (tmp_hash.value)
				free(tmp_hash.value);
			return rc;
		}

		if (cfs_crypto_hash_digestsize(hmac_alg) != tmp_hash.length) {
			free(tmp_hash.value);
			return -EINVAL;
		}

		bytes = (remain < tmp_hash.length) ? remain : tmp_hash.length;
		memcpy(keydata, tmp_hash.value, bytes);
		free(tmp_hash.value);
		remain -= bytes;
		keydata += bytes;
	}

	return 0;
}

/* Populates the sk_cred's session_key using the a Key Derviation Function (KDF)
 * based on the recommendations in NIST Special Publication SP 800-56B Rev 1
 * (Sep 2014) Section 5.5.1
 *
 * \param[in,out]	skc		Shared key credentials structure with
 *
 * \return	-1		failure
 * \return	0		success
 */
int sk_session_kdf(struct sk_cred *skc, lnet_nid_t client_nid,
		   gss_buffer_desc *client_token, gss_buffer_desc *server_token)
{
	struct sk_kernel_ctx *kctx = &skc->sc_kctx;
	gss_buffer_desc *session_key = &kctx->skc_session_key;
	gss_buffer_desc bufs[5];
	enum cfs_crypto_crypt_alg crypt_alg;
	int rc = -1;

	crypt_alg = cfs_crypto_crypt_alg(kctx->skc_crypt_alg);
	session_key->length = cfs_crypto_crypt_keysize(crypt_alg);
	session_key->value = malloc(session_key->length);
	if (!session_key->value) {
		printerr(0, "Failed to allocate memory for session key\n");
		return rc;
	}

	/* Key binding info ordering
	 * 1. Reserved for counter
	 * 1. DH shared key
	 * 2. Client's NIDs
	 * 3. Client's token
	 * 4. Server's token */
	bufs[0].value = NULL;
	bufs[0].length = 0;
	bufs[1] = skc->sc_dh_shared_key;
	bufs[2].value = &client_nid;
	bufs[2].length = sizeof(client_nid);
	bufs[3] = *client_token;
	bufs[4] = *server_token;

	return sk_kdf(&kctx->skc_session_key, &kctx->skc_shared_key, bufs,
		      5, cfs_crypto_hash_alg(kctx->skc_hmac_alg));
}

/* Uses the session key to create an HMAC key and encryption key.  In
 * integrity mode the session key used to generate the HMAC key uses
 * session information which is available on the wire but by creating
 * a session based HMAC key we can prevent potential replay as both the
 * client and server have random numbers used as part of the key creation.
 *
 * The keys used for integrity and privacy are formulated as below using
 * the session key that is the output of the key derivation function.  The
 * HMAC algorithm is determined by the shared key algorithm selected in the
 * key file.
 *
 * For ski mode:
 * Session HMAC Key = PBKDF2("Integrity", KDF derived Session Key)
 *
 * For skpi mode:
 * Session HMAC Key = PBKDF2("Integrity", KDF derived Session Key)
 * Session Encryption Key = PBKDF2("Encrypt", KDF derived Session Key)
 *
 * \param[in,out]	skc		Shared key credentials structure with
 *
 * \return	-1		failure
 * \return	0		success
 */
int sk_compute_keys(struct sk_cred *skc)
{
	struct sk_kernel_ctx *kctx = &skc->sc_kctx;
	gss_buffer_desc *session_key = &kctx->skc_session_key;
	gss_buffer_desc *hmac_key = &kctx->skc_hmac_key;
	gss_buffer_desc *encrypt_key = &kctx->skc_encrypt_key;
	enum cfs_crypto_hash_alg hmac_alg;
	enum cfs_crypto_crypt_alg crypt_alg;
	char *encrypt = "Encrypt";
	char *integrity = "Integrity";
	int rc;

	hmac_alg = cfs_crypto_hash_alg(kctx->skc_hmac_alg);
	hmac_key->length = cfs_crypto_hash_digestsize(hmac_alg);
	hmac_key->value = malloc(hmac_key->length);
	if (!hmac_key->value)
		return -ENOMEM;

	rc = PKCS5_PBKDF2_HMAC(integrity, -1, session_key->value,
			       session_key->length, SK_PBKDF2_ITERATIONS,
			       sk_hash_to_evp_md(hmac_alg),
			       hmac_key->length, hmac_key->value);
	if (rc == 0)
		return -EINVAL;

	/* Encryption key is only populated in privacy mode */
	if ((skc->sc_flags & LGSS_SVC_PRIV) == 0)
		return 0;

	crypt_alg = cfs_crypto_crypt_alg(kctx->skc_crypt_alg);
	encrypt_key->length = cfs_crypto_crypt_keysize(crypt_alg);
	encrypt_key->value = malloc(encrypt_key->length);
	if (!encrypt_key->value)
		return -ENOMEM;

	rc = PKCS5_PBKDF2_HMAC(encrypt, -1, session_key->value,
			       session_key->length, SK_PBKDF2_ITERATIONS,
			       sk_hash_to_evp_md(hmac_alg),
			       encrypt_key->length, encrypt_key->value);
	if (rc == 0)
		return -EINVAL;

	return 0;
}

static uint32_t __sk_compute_dh_key(struct sk_cred *skc,
				    const gss_buffer_desc *pub_key,
				    size_t *expected_len)
{
	gss_buffer_desc *dh_shared = &skc->sc_dh_shared_key;
	uint32_t rc = GSS_S_FAILURE;
#ifdef HAVE_OPENSSL_EVP_PKEY
	EVP_PKEY_CTX *ctx = NULL;
	EVP_PKEY *peerkey = NULL;

	peerkey = EVP_PKEY_new();
	if (!peerkey ||
	    EVP_PKEY_copy_parameters(peerkey, skc->sc_params) != 1) {
		printerr(0, "error: peerkey cannot be init\n");
		goto out_err;
	}

	if (EVP_PKEY_set1_encoded_public_key(peerkey,
					     pub_key->value,
					     pub_key->length) != 1) {
		printerr(0, "error: peerkey cannot be set\n");
		goto out_err;
	}

	ctx = EVP_PKEY_CTX_new_from_pkey(NULL, skc->sc_params, NULL);
	if (!ctx) {
		printerr(0, "error: ctx cannot be allocated\n");
		goto out_err;
	}

	if (EVP_PKEY_derive_init(ctx) != 1 ||
	    EVP_PKEY_derive_set_peer(ctx, peerkey) != 1) {
		printerr(0, "error: ctx cannot be init\n");
		goto out_err;
	}

	if (EVP_PKEY_derive(ctx, NULL, expected_len) != 1) {
		printerr(0, "error: cannot get dh length\n");
		goto out_err;
	}

	dh_shared->length = *expected_len;
	dh_shared->value = malloc(*expected_len);
	if (!dh_shared->value) {
		printerr(0, "error: cannot allocate memory for shared key\n");
		goto out_err;
	}

	if (EVP_PKEY_derive(ctx, dh_shared->value, &dh_shared->length) != 1) {
		printerr(0, "error: cannot derive dh key\n");
		ERR_print_errors_fp(stderr);
		goto out_err;
	}

	rc = GSS_S_COMPLETE;
out_err:
	EVP_PKEY_CTX_free(ctx);
	EVP_PKEY_free(peerkey);
#else /* !HAVE_OPENSSL_EVP_PKEY */
	BIGNUM *remote_pub_key;

	remote_pub_key = BN_bin2bn(pub_key->value, pub_key->length, NULL);
	if (!remote_pub_key) {
		printerr(0, "Failed to convert binary to BIGNUM\n");
		return rc;
	}

	*expected_len = DH_size(skc->sc_params);
	dh_shared->length = *expected_len;
	dh_shared->value = malloc(*expected_len);
	if (!dh_shared->value) {
		printerr(0,
			 "Failed to allocate memory for computed shared secret key\n");
		goto out_err;
	}

	/* This computes the shared key from the DHKE */
	dh_shared->length = DH_compute_key(dh_shared->value, remote_pub_key,
					   skc->sc_params);
	if (dh_shared->length == -1) {
		printerr(0, "DH key derivation failed: %s\n",
			 ERR_error_string(ERR_get_error(), NULL));
		goto out_err;
	}

	rc = GSS_S_COMPLETE;
out_err:
	BN_free(remote_pub_key);
#endif /* HAVE_OPENSSL_EVP_PKEY */
	return rc;
}

/**
 * Computes a session key based on the DH parameters from the host and its peer
 *
 * \param[in,out]	skc		Shared key credentials structure with
 *					the session key populated with the
 *					compute key
 * \param[in]		pub_key		Public key returned from peer in
 *					gss_buffer_desc
 * \return	gss error		failure
 * \return	GSS_S_COMPLETE		success
 */
uint32_t sk_compute_dh_key(struct sk_cred *skc, const gss_buffer_desc *pub_key)
{
	size_t expected_len;
	uint32_t rc;

	rc = __sk_compute_dh_key(skc, pub_key, &expected_len);
	if (rc != GSS_S_COMPLETE)
		return rc;

	if (skc->sc_dh_shared_key.length < expected_len) {
		/* there is around 1 chance out of 256 that the returned
		 * shared key is shorter than expected
		 */
		if (skc->sc_dh_shared_key.length >= expected_len - 2) {
			int shift = expected_len - skc->sc_dh_shared_key.length;

			/* if the key is short by only 1 or 2 bytes, just
			 * prepend it with 0s
			 */
			memmove((void *)(skc->sc_dh_shared_key.value + shift),
				skc->sc_dh_shared_key.value,
				skc->sc_dh_shared_key.length);
			memset(skc->sc_dh_shared_key.value, 0, shift);
		} else {
			/* if the key is really too short, return GSS_S_BAD_QOP
			 * so that the caller can retry to generate
			 */
			printerr(0,
				 "DH derivation returned a short key of %zu bytes, expected: %zu\n",
				 skc->sc_dh_shared_key.length, expected_len);
			rc = GSS_S_BAD_QOP;
		}
	}
	return rc;
}

/**
 * Creates a serialized buffer for the kernel in the order of struct
 * sk_kernel_ctx.
 *
 * \param[in,out]	skc		Shared key credentials structure
 * \param[in,out]	ctx_token	Serialized buffer for kernel.
 *					Caller must free this buffer.
 *
 * \return	0	success
 * \return	-1	failure
 */
int sk_serialize_kctx(struct sk_cred *skc, gss_buffer_desc *ctx_token)
{
	struct sk_kernel_ctx *kctx = &skc->sc_kctx;
	char *p, *end;
	size_t bufsize;

	bufsize = sizeof(*kctx) + kctx->skc_hmac_key.length +
		  kctx->skc_encrypt_key.length;

	ctx_token->value = malloc(bufsize);
	if (!ctx_token->value)
		return -1;
	ctx_token->length = bufsize;

	p = ctx_token->value;
	end = p + ctx_token->length;

	if (WRITE_BYTES(&p, end, kctx->skc_version))
		return -1;
	if (WRITE_BYTES(&p, end, kctx->skc_hmac_alg))
		return -1;
	if (WRITE_BYTES(&p, end, kctx->skc_crypt_alg))
		return -1;
	if (WRITE_BYTES(&p, end, kctx->skc_expire))
		return -1;
	if (WRITE_BYTES(&p, end, kctx->skc_host_random))
		return -1;
	if (WRITE_BYTES(&p, end, kctx->skc_peer_random))
		return -1;
	if (write_buffer(&p, end, &kctx->skc_hmac_key))
		return -1;
	if (write_buffer(&p, end, &kctx->skc_encrypt_key))
		return -1;

	printerr(2, "Serialized buffer of %zu bytes for kernel\n", bufsize);

	return 0;
}

/**
 * Decodes a netstring \a ns into array of gss_buffer_descs at \a bufs
 * up to \a numbufs.  Memory is allocated for each value and length
 * will be populated with the length
 *
 * \param[in,out]	bufs	Array of gss_buffer_descs
 * \param[in,out]	numbufs	number of gss_buffer_desc in array
 * \param[in]		ns	netstring to decode
 *
 * \return	buffers populated	success
 * \return	-1			failure
 */
int sk_decode_netstring(gss_buffer_desc *bufs, int numbufs, gss_buffer_desc *ns)
{
	char *ptr = ns->value;
	size_t remain = ns->length;
	unsigned int size;
	int digits;
	int sep;
	int rc;
	int i;

	for (i = 0; i < numbufs; i++) {
		/* read the size of first buffer */
		rc = sscanf(ptr, "%9u", &size);
		if (rc < 1)
			goto out_err;
		digits = (size) ? ceil(log10(size + 1)) : 1;

		/* sep of current string */
		sep = size + digits + 2;

		/* check to make sure it's valid */
		if (remain < sep || ptr[digits] != ':' ||
		    ptr[sep - 1] != ',')
			goto out_err;

		bufs[i].length = size;
		if (size == 0) {
			bufs[i].value = NULL;
		} else {
			bufs[i].value = malloc(size);
			if (!bufs[i].value)
				goto out_err;
			memcpy(bufs[i].value, &ptr[digits + 1], size);
		}

		remain -= sep;
		ptr += sep;
	}

	printerr(2, "Decoded netstring of %zu bytes\n", ns->length);
	return i;

out_err:
	while (i-- > 0) {
		if (bufs[i].value)
			free(bufs[i].value);
		bufs[i].length = 0;
	}
	return -1;
}

/**
 * Creates a netstring in a gss_buffer_desc that consists of all
 * the gss_buffer_desc found in \a bufs.  The netstring should be treated
 * as binary as it can contain null characters.
 *
 * \param[in]		bufs		Array of gss_buffer_desc to use as input
 * \param[in]		numbufs		Number of buffers in array
 * \param[in,out]	ns		Destination gss_buffer_desc to hold
 *					netstring
 *
 * \return	-1	failure
 * \return	0	success
 */
int sk_encode_netstring(gss_buffer_desc *bufs, int numbufs,
			gss_buffer_desc *ns)
{
	unsigned char *ptr;
	int size = 0;
	int rc;
	int i;

	/* size of string in decimal, string size, colon, and comma */
	for (i = 0; i < numbufs; i++) {

		if (bufs[i].length == 0)
			size += 3;
		else
			size += ceil(log10(bufs[i].length + 1)) +
				bufs[i].length + 2;
	}

	ns->length = size;
	ns->value = malloc(ns->length);
	if (!ns->value) {
		ns->length = 0;
		return -1;
	}

	ptr = ns->value;
	for (i = 0; i < numbufs; i++) {
		/* size */
		rc = scnprintf((char *) ptr, size, "%zu:", bufs[i].length);
		ptr += rc;

		/* contents */
		memcpy(ptr, bufs[i].value, bufs[i].length);
		ptr += bufs[i].length;

		/* delimeter */
		*ptr++ = ',';

		size -= bufs[i].length + rc + 1;

		/* should not happen */
		if (size < 0)
			abort();
	}

	printerr(2, "Encoded netstring of %zu bytes\n", ns->length);
	return 0;
}