/**
 * @file ca.c
 * 
 * @brief Implementation of ca_info_t.
 * 
 */

/*
 * Copyright (C) 2007 Andreas Steffen
 * Hochschule fuer Technik Rapperswil
 *
 * 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <pthread.h>

#include "x509.h"
#include "crl.h"
#include "ca.h"
#include "ac.h"
#include "certinfo.h"
#include "ocsp.h"

#include <library.h>
#include <debug.h>
#include <utils/linked_list.h>
#include <utils/identification.h>
#include <utils/fetcher.h>

typedef struct private_ca_info_t private_ca_info_t;

/**
 * Private data of a ca_info_t object.
 */
struct private_ca_info_t {
	/**
	 * Public interface for this ca info record
	 */
	ca_info_t public;
	
	/**
	 * Name of the ca info record
	 */
	char *name;

	/**
	 * Time when ca info record was installed
	 */
	time_t installed;

	/**
	 * Distinguished Name of the CA
	 */
	x509_t *cacert;
	
	/**
	 * List of attribute certificates
	 */
	linked_list_t *attrcerts;

	/**
	 * List of crl URIs
	 */
	linked_list_t *crluris;

	/**
	 * List of ocsp URIs
	 */
	linked_list_t *ocspuris;

	/**
	 * CRL issued by this ca
	 */
	crl_t *crl;

	/**
	 * List of certificate info records
	 */
	linked_list_t *certinfos;

	/**
	 * mutex controls access to the elements:
	 * name, crluris, ocspuris, crl, and certinfos
	 */
	pthread_mutex_t mutex;
};

/**
 * static options set by ca_info_set_options()
 */
static bool cache_crls = FALSE;
static u_int crl_check_interval = 0;

/**
 * Implements ca_info_t.equals
 */
static bool equals(const private_ca_info_t *this, const private_ca_info_t *that)
{
	return chunk_equals(this->cacert->get_keyid(this->cacert),
						that->cacert->get_keyid(that->cacert));
}

/**
 * Implements ca_info_t.equals_name_release_info
 */
static bool equals_name_release_info(private_ca_info_t *this, const char *name)
{
	bool found;

	pthread_mutex_lock(&(this->mutex));
	found = this->name != NULL && streq(this->name, name);

	if (found)
	{
		this->crluris->destroy_offset(this->crluris,
									  offsetof(identification_t, destroy));
		this->crluris = linked_list_create();

		this->ocspuris->destroy_offset(this->ocspuris,
									   offsetof(identification_t, destroy));
		this->ocspuris = linked_list_create();

		free(this->name);
		this->name = NULL;
	}

	pthread_mutex_unlock(&(this->mutex));
	return found;
}

/**
 * Implements ca_info_t.is_crl_issuer
 */
static bool is_cert_issuer(private_ca_info_t *this, const x509_t *cert)
{
	return cert->is_issuer(cert, this->cacert);
}

/**
 * Implements ca_info_t.is_crl_issuer
 */
static bool is_crl_issuer(private_ca_info_t *this, const crl_t *crl)
{
	return crl->is_issuer(crl, this->cacert);
}

/**
 * Implements ca_info_t.has_crl
 */
static bool has_crl(private_ca_info_t *this)
{
	bool found;

	pthread_mutex_lock(&(this->mutex));
	found = this->crl != NULL;
	pthread_mutex_unlock(&(this->mutex));

	return found;
}

/**
 * Implements ca_info_t.has_certinfos
 */
static bool has_certinfos(private_ca_info_t *this)
{
	bool found;

	pthread_mutex_lock(&(this->mutex));
	found = this->certinfos->get_count(this->certinfos) > 0;
	pthread_mutex_unlock(&(this->mutex));

	return found;
}

/**
 * Implements ca_info_t.add_crl
 */
static void add_crl(private_ca_info_t *this, crl_t *crl)
{
	pthread_mutex_lock(&(this->mutex));

	if (this->crl)
	{
		if (crl->is_newer(crl, this->crl))
		{
			this->crl->destroy(this->crl);
			this->crl = crl;
			DBG1("  this crl is newer - existing crl replaced");
		}
		else
		{
			crl->destroy(crl);
			DBG1("  this crl is not newer - existing crl retained");
		}
	}
	else
	{
		this->crl = crl;
		DBG2("  crl added");
	}

	pthread_mutex_unlock(&(this->mutex));
}

/**
 * Implements ca_info_t.list_crl
 */
static void list_crl(private_ca_info_t *this, FILE *out, bool utc)
{
	pthread_mutex_lock(&this->mutex);
	this->crl->list(this->crl, out, utc);
	pthread_mutex_unlock(&this->mutex);
}

/**
 * Implements ca_info_t.list_certinfos
 */
static void list_certinfos(private_ca_info_t *this, FILE *out, bool utc)
{
	iterator_t *iterator;
	certinfo_t *certinfo;
	chunk_t authkey;

	pthread_mutex_lock(&this->mutex);

	authkey = this->cacert->get_subjectKeyID(this->cacert);
	fprintf(out,"    authname:  '%D'\n", this->cacert->get_subject(this->cacert));
	fprintf(out,"    authkey:    %#B\n", &authkey);

	iterator = this->certinfos->create_iterator(this->certinfos, TRUE);
	while (iterator->iterate(iterator, (void**)&certinfo))
	{
		time_t nextUpdate, thisUpdate, now;
		chunk_t serial;
		
		now = time(NULL);
		nextUpdate = certinfo->get_nextUpdate(certinfo);
		thisUpdate = certinfo->get_thisUpdate(certinfo);
		serial = certinfo->get_serialNumber(certinfo);
		
		fprintf(out, "%#T, until %#T, ", &thisUpdate, utc, &nextUpdate, utc);
		if (now > nextUpdate)
		{
			fprintf(out, "expired (%V ago)\n", &now, &nextUpdate);
		}
		else
		{
			fprintf(out, "ok (expires in %V)\n", &now, &nextUpdate);
		}
		fprintf(out, "    serial:     %#B, %N\n", &serial,
				cert_status_names, certinfo->get_status(certinfo));
	}
	iterator->destroy(iterator);

	pthread_mutex_unlock(&this->mutex);
}

/**
 * Find an exact copy of an identification in a linked list
 */
static identification_t* find_identification(linked_list_t *list, identification_t *id)
{
	identification_t *found_id = NULL, *current_id;

	iterator_t *iterator = list->create_iterator(list, TRUE);

	while (iterator->iterate(iterator, (void**)&current_id))
	{
		if (id->equals(id, current_id))
		{
			found_id = current_id;
			break;
		}
	}
	iterator->destroy(iterator);

	return found_id;
}

/**
 * Add a unique identification to a linked list
 */
static identification_t *add_identification(linked_list_t *list, identification_t *id)
{
	identification_t *found_id = find_identification(list, id);

	if (found_id)
	{
		id->destroy(id);
		return found_id;
	}
	else
	{
		list->insert_last(list, (void*)id);
		return id;
	}
}

/**
 * Implements ca_info_t.add_crluri
 */
static void add_crluri(private_ca_info_t *this, chunk_t uri)
{
	if (uri.len < 6 ||
	   (strncasecmp(uri.ptr, "http", 4) != 0  &&
		strncasecmp(uri.ptr, "ldap", 4) != 0  &&
		strncasecmp(uri.ptr, "file", 4) != 0  &&
		strncasecmp(uri.ptr, "ftp",  3) != 0))
	{
		DBG1("  invalid crl uri '%#B'", uri);
		return;
	}
	else
	{
		identification_t *crluri = identification_create_from_encoding(ID_DER_ASN1_GN_URI, uri);

		pthread_mutex_lock(&(this->mutex));
		add_identification(this->crluris, crluri);
		pthread_mutex_unlock(&(this->mutex));
	}
}

/**
 * Implements ca_info_t.add_ocspuri
 */
static void add_ocspuri(private_ca_info_t *this, chunk_t uri)
{
	if (uri.len < 7 || strncasecmp(uri.ptr, "http", 4) != 0)
	{
		DBG1("  invalid ocsp uri '%.*s'", uri.len, uri.ptr);
		return;
	}
	else
	{
		identification_t *ocspuri = identification_create_from_encoding(ID_DER_ASN1_GN_URI, uri);

		pthread_mutex_lock(&(this->mutex));
		add_identification(this->ocspuris, ocspuri);
		pthread_mutex_unlock(&(this->mutex));
	}
}

/**
 * Implements ca_info_t.add_info.
 */
void add_info (private_ca_info_t *this, const private_ca_info_t *that)
{
	pthread_mutex_lock(&(this->mutex));

	if (this->name == NULL && that->name != NULL)
	{
		this->name = strdup(that->name);
	}

	pthread_mutex_unlock(&(this->mutex));

	{
		identification_t *uri;

		iterator_t *iterator = that->crluris->create_iterator(that->crluris, TRUE);

		while (iterator->iterate(iterator, (void**)&uri))
		{
			add_crluri(this, uri->get_encoding(uri));
		}
		iterator->destroy(iterator);
	}

	{
		identification_t *uri;

		iterator_t *iterator = that->ocspuris->create_iterator(that->ocspuris, TRUE);

		while (iterator->iterate(iterator, (void**)&uri))
		{
			add_ocspuri(this, uri->get_encoding(uri));
		}
		iterator->destroy(iterator);
	}
}

/**
 *  Implements ca_info_t.get_certificate.
 */
static x509_t* get_certificate(private_ca_info_t* this)
{
	return this->cacert;
}

/**
 * caches a crl by saving it to a given crl directory
 */
void cache_crl(private_ca_info_t* this, const char *crl_dir, crl_t *crl)
{
	char buffer[BUF_LEN];
	char *path;
	char *pos = buffer;
	int len = BUF_LEN;
	int n;

	chunk_t authKeyID = this->cacert->get_subjectKeyID(this->cacert);
	chunk_t uri;

	uri.ptr = buffer;
	uri.len = 7 + strlen(crl_dir) + 1 + 2*authKeyID.len + 4;

	if (uri.len >= BUF_LEN)	
	{
		DBG1("file uri exceeds buffer length of %d bytes - crl not saved", BUF_LEN);
		return;
	}

	/* print the file uri prefix */
	n = snprintf(pos, len, "file://");
	pos += n;  len -= n;

	/* remember the start of the path string */
	path = pos;

	/* print the default crl directory path */
	n = snprintf(pos, len, "%s/", crl_dir);
	pos += n;  len -= n;

	/* create and print a unique crl filename derived from the authKeyID */
	while (authKeyID.len-- > 0)
	{
		n = snprintf(pos, len, "%02x", *authKeyID.ptr++);
		pos += n; len -= n;
	}

	/* add the file suffix */
	n = snprintf(pos, len, ".crl");

	if (crl->write_to_file(crl, path, 0022, TRUE))
	{
		identification_t *crluri = identification_create_from_encoding(ID_DER_ASN1_GN_URI, uri);

		add_identification(this->crluris, crluri);
	}
}

/**
 *  Implements ca_info_t.verify_by_crl.
 */
static cert_status_t verify_by_crl(private_ca_info_t* this, certinfo_t *certinfo,
								   const char *crl_dir)
{
	rsa_public_key_t *issuer_public_key = this->cacert->get_public_key(this->cacert);
	bool stale;

	pthread_mutex_lock(&(this->mutex));
	if (this->crl == NULL)
	{
		stale = TRUE;
		DBG1("no crl is locally available");
	}
	else
	{
		stale = !this->crl->is_valid(this->crl);
		DBG1("crl is %s", stale? "stale":"valid");
	}

	if (stale && crl_check_interval > 0)
	{
		iterator_t *iterator = this->crluris->create_iterator(this->crluris, TRUE);
		identification_t *uri;
		
		while (iterator->iterate(iterator, (void**)&uri))
		{
			fetcher_t *fetcher;
			char uri_string[BUF_LEN];
			chunk_t uri_chunk = uri->get_encoding(uri);
			chunk_t response_chunk;

			snprintf(uri_string, BUF_LEN, "%.*s", uri_chunk.len, uri_chunk.ptr);
			fetcher = fetcher_create(uri_string);
			
			response_chunk = fetcher->get(fetcher);
			fetcher->destroy(fetcher);
			if (response_chunk.ptr != NULL)
			{
				crl_t *crl = crl_create_from_chunk(response_chunk);
		
				if (crl == NULL)
				{
					free(response_chunk.ptr);
					continue;
				}
				if (!is_crl_issuer(this, crl))
				{
					DBG1("  fetched crl has wrong issuer");
					crl->destroy(crl);
					continue;
				}
				if (!crl->verify(crl, issuer_public_key))
				{
					DBG1("fetched crl signature is invalid");
					crl->destroy(crl);
					continue;
				}
				DBG2("fetched crl signature is valid");

				if (this->crl == NULL)
				{
					this->crl = crl;
				}
				else if (crl->is_newer(crl, this->crl))
				{
					this->crl->destroy(this->crl);
					this->crl = crl;
					DBG1("this crl is newer - existing crl replaced");
				}
				else
				{
					crl->destroy(crl);
					DBG1("this crl is not newer - existing crl retained");
					continue;
				}
				if (crl->is_valid(crl))
				{
					if (cache_crls && strncasecmp(uri_string, "file", 4) != 0)
					{
						cache_crl(this, crl_dir, crl);
					}
					/* we found a valid crl and therefore exit the fetch loop */
					break;
				}
				else
				{
					DBG1("fetched crl is stale");
				}
			}
		}
		iterator->destroy(iterator);
	}

	if (this->crl)
	{
		if (!this->crl->verify(this->crl, issuer_public_key))
		{
			DBG1("crl signature is invalid");
			goto ret;
		}
		DBG2("crl signature is valid");

		this->crl->get_status(this->crl, certinfo);
	}

ret:
	pthread_mutex_unlock(&(this->mutex));
	return certinfo->get_status(certinfo);
}

/**
  * Implements ca_info_t.verify_by_ocsp.
  */
static cert_status_t verify_by_ocsp(private_ca_info_t* this,
									certinfo_t *certinfo,
									credential_store_t *credentials)
{
	bool stale;
	iterator_t *iterator;
	certinfo_t *cached_certinfo = NULL;
	int comparison = 1;

	pthread_mutex_lock(&(this->mutex));

	/* do we support OCSP at all? */
	if (this->ocspuris->get_count(this->ocspuris) == 0)
	{
		goto ret;
	}

	iterator = this->certinfos->create_iterator(this->certinfos, TRUE);

	/* find the list insertion point in alphabetical order */
	while(iterator->iterate(iterator, (void**)&cached_certinfo))
	{
		comparison = certinfo->compare_serialNumber(certinfo, cached_certinfo);

		if (comparison <= 0)
		{
			break;
		}
	}

	/* do we have a valid certinfo_t for this serial number in our cache? */
	if (comparison == 0)
	{	
		stale = cached_certinfo->get_nextUpdate(cached_certinfo) < time(NULL);
		DBG1("ocsp status in cache is %s", stale ? "stale":"fresh");
	}
	else
	{
		stale = TRUE;
		DBG1("ocsp status is not in cache");
	}

	if (stale)
	{
		ocsp_t *ocsp;

		ocsp = ocsp_create(this->cacert, this->ocspuris);
		ocsp->fetch(ocsp, certinfo, credentials);
		if (certinfo->get_status(certinfo) != CERT_UNDEFINED)
		{
			if (comparison != 0)
			{
				cached_certinfo = certinfo_create(certinfo->get_serialNumber(certinfo));

				if (comparison > 0)
				{
					iterator->insert_after(iterator, (void *)cached_certinfo);
				}
				else
				{
					iterator->insert_before(iterator, (void *)cached_certinfo);
				}
			}
			cached_certinfo->update(cached_certinfo, certinfo);
		}
		ocsp->destroy(ocsp);
	}
	else
	{
		certinfo->update(certinfo, cached_certinfo);
	}

	iterator->destroy(iterator);

ret:
	pthread_mutex_unlock(&(this->mutex));
	return certinfo->get_status(certinfo);
}

/**
 * Implements ca_info_t.purge_ocsp
 */
static void purge_ocsp(private_ca_info_t *this)
{
	pthread_mutex_lock(&(this->mutex));

	this->certinfos->destroy_offset(this->certinfos,
									offsetof(certinfo_t, destroy));
	this->certinfos = linked_list_create();

	pthread_mutex_unlock(&(this->mutex));
}

/**
 * Implements ca_info_t.destroy
 */
static void destroy(private_ca_info_t *this)
{
	this->attrcerts->destroy_offset(this->attrcerts,
								  offsetof(x509ac_t, destroy));
	this->crluris->destroy_offset(this->crluris,
								  offsetof(identification_t, destroy));
	this->ocspuris->destroy_offset(this->ocspuris,
								   offsetof(identification_t, destroy));
	this->certinfos->destroy_offset(this->certinfos,
								   offsetof(certinfo_t, destroy));
	DESTROY_IF(this->crl);
	free(this->name);
	free(this);
}

/**
 * list the info of this CA
 */
static void list(private_ca_info_t* this, FILE* out, bool utc)
{
	chunk_t chunk;
	identification_t *uri;
	iterator_t *iterator;
	bool first;
	
	pthread_mutex_lock(&(this->mutex));
	fprintf(out, "%#T", &this->installed, utc);

	if (this->name)
	{
		fprintf(out, ", \"%s\"\n", this->name);
	}
	else
	{
		fprintf(out, "\n");
	}

	fprintf(out, "    authname:  '%D'\n", this->cacert->get_subject(this->cacert));
	chunk = this->cacert->get_subjectKeyID(this->cacert);
	fprintf(out, "    authkey:    %#B\n", &chunk);
	chunk = this->cacert->get_keyid(this->cacert);
	fprintf(out, "    keyid:      %#B\n", &chunk);
	
	first = TRUE;
	iterator = this->crluris->create_iterator(this->crluris, TRUE);
	while (iterator->iterate(iterator, (void**)&uri))
	{
		fprintf(out, "    %s   '%D'\n",  first ? "crluris:":"        ", uri);
		first = FALSE;
	}
	iterator->destroy(iterator);
	
	first = TRUE;
	iterator = this->ocspuris->create_iterator(this->ocspuris, TRUE);
	while (iterator->iterate(iterator, (void**)&uri))
	{
		fprintf(out, "    %s  '%D'\n", first ? "ocspuris:":"         ", uri);
		first = FALSE;
	}
	iterator->destroy(iterator);
	pthread_mutex_unlock(&(this->mutex));
}

/*
 * Described in header.
 */
void ca_info_set_options(bool cache, u_int interval)
{
	cache_crls = cache;
	crl_check_interval = interval;
}

/*
 * Described in header.
 */
ca_info_t *ca_info_create(const char *name, x509_t *cacert)
{
	private_ca_info_t *this = malloc_thing(private_ca_info_t);
	
	/* initialize */
	this->installed = time(NULL);
	this->name = (name == NULL)? NULL:strdup(name);
	this->cacert = cacert;
	this->attrcerts = linked_list_create();
	this->crluris = linked_list_create();
	this->ocspuris = linked_list_create();
	this->certinfos = linked_list_create();
	this->crl = NULL;
	
	/* initialize the mutex */
	pthread_mutex_init(&(this->mutex), NULL);

	/* public functions */
	this->public.equals = (bool (*) (const ca_info_t*,const ca_info_t*))equals;
	this->public.equals_name_release_info = (bool (*) (ca_info_t*,const char*))equals_name_release_info;
	this->public.is_cert_issuer = (bool (*) (ca_info_t*,const x509_t*))is_cert_issuer;
	this->public.is_crl_issuer = (bool (*) (ca_info_t*,const crl_t*))is_crl_issuer;
	this->public.add_info = (void (*) (ca_info_t*,const ca_info_t*))add_info;
	this->public.add_crl = (void (*) (ca_info_t*,crl_t*))add_crl;
	this->public.has_crl = (bool (*) (ca_info_t*))has_crl;
	this->public.has_certinfos = (bool (*) (ca_info_t*))has_certinfos;
	this->public.list = (void (*) (ca_info_t*,FILE*,bool))list;
	this->public.list_crl = (void (*) (ca_info_t*,FILE*,bool))list_crl;
	this->public.list_certinfos = (void (*) (ca_info_t*,FILE*,bool))list_certinfos;
	this->public.add_crluri = (void (*) (ca_info_t*,chunk_t))add_crluri;
	this->public.add_ocspuri = (void (*) (ca_info_t*,chunk_t))add_ocspuri;
	this->public.get_certificate = (x509_t* (*) (ca_info_t*))get_certificate;
	this->public.verify_by_crl = (cert_status_t (*) (ca_info_t*,certinfo_t*, const char*))verify_by_crl;
	this->public.verify_by_ocsp = (cert_status_t (*) (ca_info_t*,certinfo_t*,credential_store_t*))verify_by_ocsp;
	this->public.purge_ocsp = (void (*) (ca_info_t*))purge_ocsp;
	this->public.destroy = (void (*) (ca_info_t*))destroy;

	return &this->public;
}
