/*
 * Security functions for iSNS
 *
 * Copyright (C) 2007 Olaf Kirch <olaf.kirch@oracle.com>
 */

#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "config.h"
#include <libisns/isns.h>
#include "security.h"
#include <libisns/source.h>
#include <libisns/util.h>

#ifdef WITH_SECURITY

#if OPENSSL_VERSION_NUMBER < 0x10100000L
#define EVP_PKEY_base_id(o) ((o)->type)
#endif

/*
 * Allocate a security peer
 */
static isns_principal_t *
isns_create_principal(const char *spi, size_t spi_len, EVP_PKEY *pk)
{
	char		keydesc[32];
	isns_principal_t *peer;

	peer = isns_calloc(1, sizeof(*peer));
	peer->is_users = 1;
	if (spi) {
		peer->is_name = isns_malloc(spi_len + 1);
		memcpy(peer->is_name, spi, spi_len);
		peer->is_name[spi_len] = '\0';
		peer->is_namelen = spi_len;
	}

	peer->is_key = pk;
	if (pk) {
		const char	*algo;

		switch (EVP_PKEY_base_id(pk)) {
		case EVP_PKEY_DSA: algo = "DSA"; break;
		case EVP_PKEY_RSA: algo = "RSA"; break;
		default: algo = "unknown"; break;
		}

		snprintf(keydesc, sizeof(keydesc), " (%s/%u)",
				algo, EVP_PKEY_bits(pk));
	}

	isns_debug_auth("Created security principal \"%s\"%s\n",
			peer->is_name, keydesc);
	return peer;
}

static void
isns_principal_set_key(isns_principal_t *princ, EVP_PKEY *key)
{
	if (princ->is_key == key)
		return;
	if (princ->is_key)
		EVP_PKEY_free(princ->is_key);
	princ->is_key = key;
}

void
isns_principal_free(isns_principal_t *peer)
{
	if (!peer)
		return;

	isns_assert(peer->is_users);
	if (--(peer->is_users))
		return;

	if (peer->is_name)
		isns_free(peer->is_name);
	if (peer->is_key)
		EVP_PKEY_free(peer->is_key);
	isns_policy_release(peer->is_policy);
	isns_free(peer);
}

/*
 * Set the principal's name
 */
void
isns_principal_set_name(isns_principal_t *princ, const char *spi)
{
	isns_assign_string(&princ->is_name, spi);
	isns_debug_auth("Setting principal name to \"%s\"\n", spi);
}

const char *
isns_principal_name(const isns_principal_t *princ)
{
	return princ->is_name;
}

/*
 * Cache policy in the principal object.
 */
void
isns_principal_set_policy(isns_principal_t *princ,
		isns_policy_t *policy)
{
	if (policy)
		policy->ip_users++;
	isns_policy_release(princ->is_policy);
	princ->is_policy = policy;
}

/*
 * Key management functions for a security context.
 */
isns_principal_t *
isns_security_load_privkey(isns_security_t *ctx, const char *filename)
{
	EVP_PKEY	*pkey;

	isns_debug_auth("Loading private %s key from %s\n",
				ctx->is_name, filename);
	if (!ctx->is_load_private)
		return NULL;
	if (!(pkey = ctx->is_load_private(ctx, filename))) {
		isns_error("Unable to load private %s key from %s\n",
				ctx->is_name, filename);
		return NULL;
	}

	return isns_create_principal(NULL, 0, pkey);
}

isns_principal_t *
isns_security_load_pubkey(isns_security_t *ctx, const char *filename)
{
	EVP_PKEY	*pkey;

	isns_debug_auth("Loading public %s key from %s\n",
				ctx->is_name, filename);
	if (!ctx->is_load_public)
		return NULL;
	if (!(pkey = ctx->is_load_public(ctx, filename))) {
		isns_error("Unable to load public %s key from %s\n",
				ctx->is_name, filename);
		return NULL;
	}

	return isns_create_principal(NULL, 0, pkey);
}

void
isns_security_set_identity(isns_security_t *ctx, isns_principal_t *princ)
{
	if (princ)
		princ->is_users++;
	if (ctx->is_self)
		isns_principal_free(ctx->is_self);
	ctx->is_self = princ;
}

void
isns_add_principal(isns_security_t *ctx, isns_principal_t *princ)
{
	if (princ)
		princ->is_users++;
	princ->is_next = ctx->is_peers;
	ctx->is_peers = princ;
}

isns_principal_t *
isns_get_principal(isns_security_t *ctx, const char *spi, size_t spi_len)
{
	isns_principal_t *princ;
	isns_policy_t	*policy;
	isns_keystore_t *ks;
	EVP_PKEY	*pk;

	ks = ctx->is_peer_keys;

	for (princ = ctx->is_peers; princ; princ = princ->is_next) {
		/* In a client socket, we set the (expected)
		 * public key of the peer through
		 * isns_security_set_peer_key, which will
		 * just put it on the peers list.
		 * This key usually has no name.
		 */
		if (princ->is_name == NULL) {
			princ->is_users++;
			return princ;
		}
		if (spi_len == princ->is_namelen
		 && !memcmp(princ->is_name, spi, spi_len)) {
			/* Check whether the cached key and policy
			 * might be stale. */
			if (ks && ks->ic_generation != princ->is_generation) {
				pk = ks->ic_find(ks, spi, spi_len);
				if (pk == NULL) {
					isns_debug_auth("Unable to refresh key "
						"for principal %.*s - probably deleted\n",
						spi_len, spi);
					return NULL;
				}
				isns_debug_auth("Refresh key for principal %.*s\n",
						spi_len, spi);
				isns_principal_set_key(princ, pk);
				princ->is_users++;
				goto refresh_policy;
			}
			princ->is_users++;
			return princ;
		}
	}

	if ((ks = ctx->is_peer_keys) == NULL)
		return NULL;

	if (!(pk = ks->ic_find(ks, spi, spi_len)))
		return NULL;
	princ = isns_create_principal(spi, spi_len, pk);

	/* Add it to the list */
	princ->is_next = ctx->is_peers;
	ctx->is_peers = princ;
	princ->is_users++;

	/* Bind the policy for this peer */
refresh_policy:
	if (!ks->ic_get_policy
	 || !(policy = ks->ic_get_policy(ks, spi, spi_len)))
		policy = isns_policy_default(spi, spi_len);

	/* If no entity is set, use the SPI */
	if (policy->ip_entity == NULL)
		isns_assign_string(&policy->ip_entity, policy->ip_name);

	/* If the list of permitted node names is empty,
	 * default to the standard pattern derived from
	 * the reversed entity name */
	if (policy->ip_node_names.count == 0) {
		char	*pattern;

		pattern = isns_build_source_pattern(policy->ip_entity);
		if (pattern != NULL)
			isns_string_array_append(&policy->ip_node_names,
					pattern);
		isns_free(pattern);
	}

	isns_principal_set_policy(princ, policy);
	isns_policy_release(policy);

	/* Remember the keystore generation number */
	princ->is_generation = ks->ic_generation;

	return princ;
}

/*
 * Create a keystore for a security context.
 * Key stores let the server side retrieve the
 * keys associated with a given SPI.
 *
 * For now, we support just simple key stores,
 * but this could be extended to support
 * URLs such as ldaps://ldap.example.com
 */
isns_keystore_t *
isns_create_keystore(const char *spec)
{
	if (*spec != '/')
		return NULL;

	return isns_create_simple_keystore(spec);
}

/*
 * Attach the keystore to the security context
 */
void
isns_security_set_keystore(isns_security_t *ctx,
			isns_keystore_t *ks)
{
	ctx->is_peer_keys = ks;
}

/*
 * Check that the client supplied time stamp is within a
 * certain window.
 */
static int
isns_security_check_timestamp(isns_security_t *ctx,
					isns_principal_t *peer,
					uint64_t timestamp)
{
	int64_t	delta;

	/* The time stamp must not be earlier than timestamp_jitter
	 * before the last message received. */
	if (peer->is_timestamp) {
		delta = timestamp - peer->is_timestamp;
		if (delta < -(int64_t) ctx->is_timestamp_jitter)
			return 0;
	}

	/* We allow the client's clock to diverge from ours, within
	 * certain limits. */
	if (ctx->is_replay_window != 0) {
		time_t	now = time(NULL);

		delta = timestamp - now;
		if (delta < 0)
			delta = -delta;
		if (delta > ctx->is_replay_window)
			return 0;
	}

	peer->is_timestamp = timestamp;
	return 1;
}

int
isns_security_sign(isns_security_t *ctx, isns_principal_t *peer,
		buf_t *bp, struct isns_authblk *auth)
{
	if (!ctx->is_sign) {
		isns_debug_auth("isns_security_sign: auth context without "
				"sign handler.\n");
		return 0;
	}
	if (!ctx->is_sign(ctx, peer, bp, auth)) {
		isns_debug_auth("Failed to sign message, spi=%s\n",
				peer->is_name);
		return 0;
	}

	return 1;
}

int
isns_security_verify(isns_security_t *ctx, isns_principal_t *peer,
		buf_t *bp, struct isns_authblk *auth)
{
	if (!isns_security_check_timestamp(ctx, peer, auth->iab_timestamp)) {
		isns_debug_auth("Possible replay attack (bad timestamp) "
				"from spi=%s\n", peer->is_name);
		return 0;
	}

	if (!ctx->is_verify) {
		isns_debug_auth("isns_security_verify: auth context without "
				"verify handler.\n");
		return 0;
	}
	if (!ctx->is_verify(ctx, peer, bp, auth)) {
		isns_debug_auth("Failed to authenticate message, spi=%s\n",
				peer->is_name);
		return 0;
	}

	return 1;
}

/*
 * Initialize security services.
 */
int
isns_security_init(void)
{
	if (!isns_config.ic_dsa.param_file) {
		isns_error("No DSA parameter file - please edit configuration\n");
		return 0;
	}

	if (!isns_dsa_init_params(isns_config.ic_dsa.param_file))
		return 0;

	if (!isns_config.ic_auth_key_file) {
		isns_error("No AuthKey specified; please edit configuration\n");
		return 0;
	}

	if (!isns_dsa_init_key(isns_config.ic_auth_key_file))
		return 0;

	return 1;
}

#else /* WITH_SECURITY */

static void
isns_no_security(void)
{
	static int complain = 0;

	if (complain++ < 5)
		isns_error("iSNS authentication disabled in this build\n");
}

int
isns_security_init(void)
{
	isns_no_security();
	return 0;
}

isns_keystore_t *
isns_create_keystore(__attribute__((unused))const char *spec)
{
	isns_no_security();
	return NULL;
}

void
isns_security_set_keystore(__attribute__((unused))isns_security_t *ctx,
			   __attribute__((unused))isns_keystore_t *ks)
{
	isns_no_security();
}

void
isns_principal_free(__attribute__((unused))isns_principal_t *peer)
{
}

isns_principal_t *
isns_get_principal(__attribute__((unused))isns_security_t *ctx,
		   __attribute__((unused))const char *spi,
		   __attribute__((unused))size_t spi_len)
{
	return NULL;
}

const char *
isns_principal_name(__attribute__((unused))const isns_principal_t *princ)
{
	return NULL;
}

#endif /* WITH_SECURITY */
