/* $Id$ 
 *
 * Lasso - A free implementation of the Liberty Alliance specifications.
 *
 * Copyright (C) 2004, 2005 Entr'ouvert
 * http://lasso.entrouvert.org
 * 
 * Authors: See AUTHORS file in top-level directory.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <string.h>
#include <time.h>

#include <libxml/uri.h>

#include <openssl/pem.h>
#include <openssl/sha.h>
#include <openssl/engine.h>

#include <xmlsec/base64.h>
#include <xmlsec/crypto.h>
#include <xmlsec/templates.h>
#include <xmlsec/xmldsig.h>
#include <xmlsec/xmltree.h>

#include <lasso/xml/xml.h>

/**
 * lasso_build_random_sequence:
 * @buffer: buffer to fill with random sequence
 * @size: the sequence size in byte (character)
 * 
 * Builds a random sequence of [0-9A-F] characters of size @size.
 * 
 * Return value: None
 **/
void
lasso_build_random_sequence(char *buffer, unsigned int size)
{
	char *t;
	unsigned int rnd, i;

	t = buffer;
	while (t-buffer < (int)size) {
		rnd = g_random_int();
		for (i=0; i<sizeof(int); i++) {
			*(t++) = '0' + ((rnd>>i*4)&0xf);
			if (*(t-1) > '9') *(t-1) += 7;
		}
	}
}

/**
 * lasso_build_unique_id:
 * @size: the ID's length (between 32 and 40)
 * 
 * Builds an ID which has an unicity probability of 2^(-size*4).
 * 
 * Return value: a "unique" ID (begin always with _ character)
 **/
char*
lasso_build_unique_id(unsigned int size)
{
	/*
	 * When generating one-time-use identifiers for Principals, in the
	 * case that a pseudorandom technique is employed, the probability
	 * of two randomly chosen identifiers being identical MUST be less
	 * than or equal to 2-128 and SHOULD be less than or equal to 2-160.
	 * These levels correspond, respectively, to use of strong 128-bit
	 * and 160-bit hash functions, in conjunction with sufficient input
	 * entropy.
	 *   -- 3.1.4 Name Identifier Construction
	 *      in « Liberty ID-FF Protocols and Schema Specification »
	 */
	char *result;

	g_assert(size >= 32);

	result = g_malloc(size+2); /* trailing \0 and leading _ */
	result[0] = '_';
	lasso_build_random_sequence(result+1, size);
	result[size+1] = 0;
	return result;
}

/**
 * lasso_get_current_time:
 * 
 * Returns the current time, format is "yyyy-mm-ddThh:mm:ssZ".
 * 
 * Return value: a string
 **/
char*
lasso_get_current_time()
{
	time_t now;
	struct tm *tm;
	char *ret;

	ret = g_malloc(21);
	now = time(NULL);
	tm = gmtime(&now);
	strftime(ret, 21, "%Y-%m-%dT%H:%M:%SZ", tm);

	return ret;
}

/**
 * lasso_get_pem_file_type:
 * @pem_file: a pem file
 * 
 * Gets the type of a pem file.
 * 
 * Return value: the pem file type
 **/
LassoPemFileType
lasso_get_pem_file_type(const char *pem_file)
{
	BIO* bio;
	EVP_PKEY *pkey;
	X509 *cert;
	LassoPemFileType type = LASSO_PEM_FILE_TYPE_UNKNOWN;

	g_return_val_if_fail(pem_file != NULL, LASSO_PARAM_ERROR_INVALID_VALUE);

	bio = BIO_new_file(pem_file, "rb");
	if (bio == NULL) {
		message(G_LOG_LEVEL_CRITICAL, "Failed to open %s pem file", pem_file);
		return -1;
	}

	pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
	if (pkey != NULL) {
		type = LASSO_PEM_FILE_TYPE_PUB_KEY;
		EVP_PKEY_free(pkey);
	} else {
		BIO_reset(bio);
		pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
		if (pkey != NULL) {
			type = LASSO_PEM_FILE_TYPE_PRIVATE_KEY;
			EVP_PKEY_free(pkey);
		} else {
			BIO_reset(bio);
			cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
			if (cert != NULL) {
				type = LASSO_PEM_FILE_TYPE_CERT;
				X509_free(cert);
			}
		}
	}
	BIO_free(bio);

	return type;
}

/**
 * lasso_get_public_key_from_pem_cert_file:
 * @pem_cert_file: an X509 pem certificate file
 * 
 * Gets the public key in an X509 pem certificate file.
 * 
 * Return value: a public key or NULL if an error occurs.
 **/
xmlSecKeyPtr
lasso_get_public_key_from_pem_cert_file(const char *pem_cert_file)
{
	FILE *fd;
	X509 *pem_cert;
	xmlSecKeyDataPtr data;
	xmlSecKeyPtr key = NULL;

	g_return_val_if_fail(pem_cert_file != NULL, NULL);

	/* load pem certificate from file */
	fd = fopen(pem_cert_file, "r");
	if (fd == NULL) {
		message(G_LOG_LEVEL_CRITICAL, "Failed to open %s pem certificate file",
				pem_cert_file);
		return NULL;
	}
	/* read the pem X509 certificate */
	pem_cert = PEM_read_X509(fd, NULL, NULL, NULL);
	fclose(fd);
	if (pem_cert == NULL) {
		message(G_LOG_LEVEL_CRITICAL, "Failed to read X509 certificate");
		return NULL;
	}

	/* get public key value in certificate */
	data = xmlSecOpenSSLX509CertGetKey(pem_cert);
	if (data != NULL) {
		/* create key and set key value */
		key = xmlSecKeyCreate();
		xmlSecKeySetValue(key, data);
	} else {
		message(G_LOG_LEVEL_CRITICAL,
				"Failed to get the public key in the X509 certificate");
	}
	X509_free(pem_cert);

	return key;
}

/**
 * lasso_load_certs_from_pem_certs_chain_file:
 * @pem_certs_chain_file: a CA certificate chain file
 * 
 * Creates a keys manager and loads inside all the CA certificates of
 * @pem_certs_chain_file. Caller is responsible for freeing it with
 * xmlSecKeysMngrDestroy() function.
 * 
 * Return value: a newly allocated keys manager or NULL if an error occurs.
 **/
xmlSecKeysMngrPtr
lasso_load_certs_from_pem_certs_chain_file(const char* pem_certs_chain_file)
{
	xmlSecKeysMngrPtr keys_mngr;
	GIOChannel *gioc;
	gchar *line;
	gsize len, pos;
	GString *cert = NULL;
	gint ret;

	g_return_val_if_fail(pem_certs_chain_file != NULL, NULL);

	/* create keys manager */
	keys_mngr = xmlSecKeysMngrCreate();
	if (keys_mngr == NULL) {
		message(G_LOG_LEVEL_CRITICAL,
				lasso_strerror(LASSO_DS_ERROR_KEYS_MNGR_CREATION_FAILED));
		return NULL;
	}
	/* initialize keys manager */
	if (xmlSecCryptoAppDefaultKeysMngrInit(keys_mngr) < 0) {
		message(G_LOG_LEVEL_CRITICAL,
				lasso_strerror(LASSO_DS_ERROR_KEYS_MNGR_INIT_FAILED));
		xmlSecKeysMngrDestroy(keys_mngr);
		return NULL;
	}    

	gioc = g_io_channel_new_file(pem_certs_chain_file, "r", NULL);
	while (g_io_channel_read_line(gioc, &line, &len, &pos, NULL) == G_IO_STATUS_NORMAL) {
		if (g_strstr_len(line, 64, "BEGIN CERTIFICATE") != NULL) {
			cert = g_string_new(line);
		} else if (g_strstr_len(line, 64, "END CERTIFICATE") != NULL) {
			g_string_append(cert, line);
			/* load the new certificate found in the keys manager */
			ret = xmlSecCryptoAppKeysMngrCertLoadMemory(keys_mngr,
					(const xmlSecByte*) cert->str,
					(xmlSecSize) cert->len,
					xmlSecKeyDataFormatPem,
					xmlSecKeyDataTypeTrusted);
			g_string_free(cert, TRUE);
			cert = NULL;
			if (ret < 0) {
				if (line) {
					g_free(line);
					xmlSecKeysMngrDestroy(keys_mngr);
				}
				g_io_channel_shutdown(gioc, TRUE, NULL);
				return NULL;
			}
		} else if (cert != NULL && line != NULL && line[0] != '\0') {
			g_string_append(cert, line);
		} else {
			debug("Empty line found in the CA certificate chain file");
		}
		/* free last line read */
		if (line != NULL) {
			g_free(line);
			line = NULL;
		}
	}

	g_io_channel_shutdown(gioc, TRUE, NULL);

	return keys_mngr;
}

/**
 * lasso_query_sign:
 * @query: a query (an url-encoded node)
 * @sign_method: the Signature transform method
 * @private_key_file: the private key
 * 
 * Signs a query (url-encoded message).
 * 
 * Return value: a newly allocated query signed or NULL if an error occurs.
 **/
xmlChar*
lasso_query_sign(xmlChar *query, LassoSignatureMethod sign_method, const char *private_key_file)
{
	BIO *bio = NULL;
	xmlChar *digest = NULL; /* 160 bit buffer */
	RSA *rsa = NULL;
	DSA *dsa = NULL;
	unsigned char *sigret = NULL;
	unsigned int siglen;
	xmlChar *b64_sigret = NULL, *e_b64_sigret = NULL;
	xmlChar *new_query = NULL, *s_new_query = NULL;
	int status = 0;
	char *t;

	g_return_val_if_fail(query != NULL, NULL);
	g_return_val_if_fail(sign_method == LASSO_SIGNATURE_METHOD_RSA_SHA1 ||
			sign_method == LASSO_SIGNATURE_METHOD_DSA_SHA1, NULL);
	g_return_val_if_fail(private_key_file != NULL, NULL);

	bio = BIO_new_file(private_key_file, "rb");
	if (bio == NULL) {
		message(G_LOG_LEVEL_CRITICAL, "Failed to open %s private key file",
				private_key_file);
		return NULL;
	}

	/* add SigAlg */
	switch (sign_method) {
		case LASSO_SIGNATURE_METHOD_RSA_SHA1:
			t = xmlURIEscapeStr(xmlSecHrefRsaSha1, NULL);
			new_query = g_strdup_printf("%s&SigAlg=%s", query, t);
			xmlFree(t);
			break;
		case LASSO_SIGNATURE_METHOD_DSA_SHA1:
			t = xmlURIEscapeStr(xmlSecHrefDsaSha1, NULL);
			new_query = g_strdup_printf("%s&SigAlg=%s", query, t);
			xmlFree(t);
			break;
	}

	/* build buffer digest */
	digest = lasso_sha1(new_query);
	if (digest == NULL) {
		message(G_LOG_LEVEL_CRITICAL, "Failed to build the buffer digest");
		goto done;
	}

	/* calculate signature value */
	if (sign_method == LASSO_SIGNATURE_METHOD_RSA_SHA1) {
		/* load private key */
		rsa = PEM_read_bio_RSAPrivateKey(bio, NULL, NULL, NULL);
		if (rsa == NULL) {
			goto done;
		}
		/* alloc memory for sigret */
		sigret = (unsigned char *)g_malloc (RSA_size(rsa));
		/* sign digest message */
		status = RSA_sign(NID_sha1, digest, 20, sigret, &siglen, rsa);
		RSA_free(rsa);
	}
	else if (sign_method == LASSO_SIGNATURE_METHOD_DSA_SHA1) {
		dsa = PEM_read_bio_DSAPrivateKey(bio, NULL, NULL, NULL);
		if (dsa == NULL) {
			goto done;
		}
		sigret = (unsigned char *)g_malloc (DSA_size(dsa));
		status = DSA_sign(NID_sha1, digest, 20, sigret, &siglen, dsa);
		DSA_free(dsa);
	}
	if (status == 0) {
		goto done;
	}

	/* Base64 encode the signature value */
	b64_sigret = xmlSecBase64Encode(sigret, siglen, 0);
	/* escape b64_sigret */
	e_b64_sigret = xmlURIEscapeStr(b64_sigret, NULL);

	/* add signature */
	switch (sign_method) {
		case LASSO_SIGNATURE_METHOD_RSA_SHA1:
			s_new_query = g_strdup_printf("%s&Signature=%s", new_query, e_b64_sigret);
			break;
		case LASSO_SIGNATURE_METHOD_DSA_SHA1:
			s_new_query = g_strdup_printf("%s&Signature=%s", new_query, e_b64_sigret);
			break;
	}

done:
	g_free(new_query);
	xmlFree(digest);
	BIO_free(bio);
	g_free(sigret);
	xmlFree(b64_sigret);
	xmlFree(e_b64_sigret);

	return s_new_query;
}

/**
 * lasso_query_verify_signature:
 * @query: a query (an url-encoded message)
 * @sender_public_key: the query sender public key
 * 
 * Verifies the query signature.
 * 
 * Return value: 0 if signature is valid
 * a positive value if signature was not found or is invalid
 * a negative value if an error occurs during verification
 **/
int
lasso_query_verify_signature(const char *query, const xmlSecKey *sender_public_key)
{
	RSA *rsa = NULL;
	DSA *dsa = NULL;
	gchar **str_split = NULL;
	xmlChar *digest = NULL, *b64_signature = NULL;
	xmlSecByte *signature = NULL;
	int key_size, status = 0, ret = 0;
	char *sig_alg, *usig_alg = NULL;

	g_return_val_if_fail(query != NULL, LASSO_PARAM_ERROR_INVALID_VALUE);
	g_return_val_if_fail(sender_public_key != NULL, LASSO_PARAM_ERROR_INVALID_VALUE);
	g_return_val_if_fail(sender_public_key->value != NULL, LASSO_PARAM_ERROR_INVALID_VALUE);

	/* split query, the signature MUST be the last param of the query
	 * actually there could be more params in the URL; but they wouldn't be
	 * covered by the signature */

	str_split = g_strsplit(query, "&Signature=", 0);
	if (str_split[1] == NULL) {
		g_strfreev(str_split);
		return LASSO_DS_ERROR_SIGNATURE_NOT_FOUND;
	}

	if (sender_public_key->value->id == xmlSecOpenSSLKeyDataRsaId) {
	} else {
		/* no key; it will fail later */
	}

	sig_alg = strstr(str_split[0], "&SigAlg=");
	if (sig_alg == NULL) {
		ret = critical_error(LASSO_DS_ERROR_INVALID_SIGALG);
		goto done;
	}
	sig_alg = strchr(sig_alg, '=')+1;

	usig_alg = xmlURIUnescapeString(sig_alg, 0, NULL);
	if (strcmp(usig_alg, xmlSecHrefRsaSha1) == 0) {
		if (sender_public_key->value->id != xmlSecOpenSSLKeyDataRsaId) {
			ret = critical_error(LASSO_DS_ERROR_PUBLIC_KEY_LOAD_FAILED);
			goto done;
		}
		rsa = xmlSecOpenSSLKeyDataRsaGetRsa(sender_public_key->value);
		if (rsa == NULL) {
			ret = critical_error(LASSO_DS_ERROR_PUBLIC_KEY_LOAD_FAILED);
			goto done;
		}
		key_size = RSA_size(rsa);
	} else if (strcmp(usig_alg, xmlSecHrefDsaSha1) == 0) {
		if (sender_public_key->value->id != xmlSecOpenSSLKeyDataDsaId) {
			ret = critical_error(LASSO_DS_ERROR_PUBLIC_KEY_LOAD_FAILED);
			goto done;
		}
		dsa = xmlSecOpenSSLKeyDataDsaGetDsa(sender_public_key->value);
		if (dsa == NULL) {
			ret = critical_error(LASSO_DS_ERROR_PUBLIC_KEY_LOAD_FAILED);
			goto done;
		}
		key_size = DSA_size(dsa);
	} else {
		ret = critical_error(LASSO_DS_ERROR_INVALID_SIGALG);
		goto done;
	}

	/* insure there is only the signature in str_split[1] */
	if (strchr(str_split[1], '&')) {
		strchr(str_split[1], '&')[0] = 0;
	}

	/* get signature (unescape + base64 decode) */
	signature = xmlMalloc(key_size+1);
	b64_signature = xmlURIUnescapeString(str_split[1], 0, NULL);
	xmlSecBase64Decode(b64_signature, signature, key_size+1);

	/* compute signature digest */
	digest = lasso_sha1(str_split[0]);
	if (digest == NULL) {
		ret = critical_error(LASSO_DS_ERROR_DIGEST_COMPUTE_FAILED);
		goto done;
	}

	if (rsa) {
		status = RSA_verify(NID_sha1, digest, 20, signature, key_size, rsa);
	} else if (dsa) {
		status = DSA_verify(NID_sha1, digest, 20, signature, key_size, dsa);
	}

	if (status == 0) {
		ret = LASSO_DS_ERROR_INVALID_SIGNATURE;
	}

done:
	xmlFree(b64_signature);
	xmlFree(signature);
	xmlFree(digest);
	xmlFree(usig_alg);
	g_strfreev(str_split);

	return ret;
}

/**
 * lasso_sha1:
 * @str: a string
 * 
 * Builds the SHA-1 message digest (cryptographic hash) of @str
 * 
 * Return value: 20-bytes buffer allocated with xmlMalloc
 **/
char*
lasso_sha1(const char *str)
{
	xmlChar *md;

	if (str == NULL)
		return NULL;

	md = xmlMalloc(20);
	return SHA1(str, strlen(str), md);
}

char**
urlencoded_to_strings(const char *str)
{
	int i, n=1;
	char *st, *st2;
	char **result;

	st = (char*)str;
	while (strchr(st, '&')) {
		st = strchr(st, '&')+1;
		n++;
	}

	result = g_malloc(sizeof(char*)*(n+1));
	result[n] = NULL;

	st = (char*)str;
	for (i=0; i<n; i++) {
		st2 = strchr(st, '&');
		st2 = st2 ? st2 : st+strlen(st);
		result[i] = xmlURIUnescapeString(st, st2-st, NULL);
		st = st2 + 1;
	}
	return result;
}

void
_debug(GLogLevelFlags level, const char *filename, int line,
		const char *function, const char *format, ...) 
{
	char debug_string[1024];
	time_t ts;
	char date[20];
	va_list args;

	va_start(args, format);
	g_vsnprintf(debug_string, 1024, format, args);
	va_end(args);

	time(&ts);
	strftime(date, 20, "%Y-%m-%d %H:%M:%S", localtime(&ts));

	if (level == G_LOG_LEVEL_DEBUG || level == G_LOG_LEVEL_CRITICAL) {
		g_log("Lasso", level, "%s (%s/%s:%d)\n======> %s",
				date, filename, function, line, debug_string);
	} else {
		g_log("Lasso", level, "%s\t%s", date, debug_string);
	}
}

int
error_code(GLogLevelFlags level, int error, ...)
{
	const char *format;
	char message[1024];
	va_list args;

	format = lasso_strerror(error);

	va_start(args, error);
	g_vsnprintf(message, 1024, format, args);
	va_end(args);

	return error;
}


int
lasso_sign_node(xmlNode *xmlnode, const char *id_attr_name, const char *id_value,
		const char *private_key_file, const char *certificate_file)
{
	xmlDoc *doc;
	xmlNode *sign_tmpl, *old_parent;
	xmlSecDSigCtx *dsig_ctx;

	sign_tmpl = NULL;
	for (sign_tmpl = xmlnode->children; sign_tmpl; sign_tmpl = sign_tmpl->next) {
		if (strcmp(sign_tmpl->name, "Signature") == 0)
			break;
	}
	sign_tmpl = xmlSecFindNode(xmlnode, xmlSecNodeSignature, xmlSecDSigNs);

	if (sign_tmpl == NULL)
		return LASSO_DS_ERROR_SIGNATURE_TEMPLATE_NOT_FOUND;

	doc = xmlNewDoc("1.0");
	old_parent = xmlnode->parent;
	xmlnode->parent = NULL;
	xmlDocSetRootElement(doc, xmlnode);
	xmlSetTreeDoc(sign_tmpl, doc);
	if (id_attr_name) {
		xmlAttr *id_attr = xmlHasProp(xmlnode, id_attr_name);
		if (id_value) {
			xmlAddID(NULL, doc, id_value, id_attr);
		}
	}

	dsig_ctx = xmlSecDSigCtxCreate(NULL);
	dsig_ctx->signKey = xmlSecCryptoAppKeyLoad(private_key_file,
			xmlSecKeyDataFormatPem,
			NULL, NULL, NULL);
	if (dsig_ctx->signKey == NULL) {
		xmlSecDSigCtxDestroy(dsig_ctx);
		return critical_error(LASSO_DS_ERROR_PRIVATE_KEY_LOAD_FAILED);
	}
	if (certificate_file != NULL && certificate_file[0] != 0) {
		if (xmlSecCryptoAppKeyCertLoad(dsig_ctx->signKey, certificate_file,
					xmlSecKeyDataFormatPem) < 0) {
			xmlSecDSigCtxDestroy(dsig_ctx);
			return critical_error(LASSO_DS_ERROR_CERTIFICATE_LOAD_FAILED);
		}
	}
	if (xmlSecDSigCtxSign(dsig_ctx, sign_tmpl) < 0) {
		xmlSecDSigCtxDestroy(dsig_ctx);
		return critical_error(LASSO_DS_ERROR_SIGNATURE_FAILED);
	}
	xmlSecDSigCtxDestroy(dsig_ctx);
	xmlUnlinkNode(xmlnode);
	xmlnode->parent = old_parent;
	xmlFreeDoc(doc);

	return 0;
}

