/* Support of X.509 certificates and CRLs
 * Copyright (C) 2000 Andreas Hess, Patric Lichtsteiner, Roger Wegmann
 * Copyright (C) 2001 Marco Bertossa, Andreas Schleiss
 * Copyright (C) 2002 Mario Strasser
 * Copyright (C) 2000-2004 Andreas Steffen, Zuercher Hochschule Winterthur
 * Copyright (C) 2010 Paul Wouters <paul@xelerance.com>
 *
 *
 * 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 <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <time.h>
#include <limits.h>
#include <sys/types.h>

#include <openswan.h>
#include <openswan/ipsec_policy.h>

#include "sysdep.h"
#include "oswalloc.h"
#include "oswconf.h"
#include "constants.h"
#include "oswlog.h"
#include "oswtime.h"

#include "id.h"
#include "asn1.h"
#include "oid.h"
#include "x509.h"
#include "pgp.h"
#include "certs.h"
#include "packet.h"
#include "md2.h"
#include "md5.h"
#include "sha1.h"
#include "pkcs.h"
#include "paths.h"

/* Maximum length of ASN.1 distinquished name */
#define ASN1_BUF_LEN	      512

/*
 * Filter eliminating the directory entries starting with .,
 * and also "CVS" (thus eliminating '.' and '..')
 */
int
file_select(
#ifdef SCANDIR_HAS_CONST	    
	    const
#endif
	    struct dirent *entry)
{
  return (entry->d_name[0] != '.' &&
	  strcmp(entry->d_name, "CVS")!=0 &&
	  strcmp(entry->d_name, "RCS")!=0);
	  
}

/*
 * AUTH CERTIFICATE chains
 */
static x509cert_t *x509authcerts = NULL;

/*const*/ x509cert_t *x509_get_authcerts_chain(void)
{
    return x509authcerts;
}

/*
 *  get a X.509 authority certificate with a given subject or keyid
 */
x509cert_t*
get_authcert(chunk_t subject, chunk_t serial, chunk_t keyid, u_char auth_flags)
{
    x509cert_t *cert = x509authcerts;
    x509cert_t *prev_cert = NULL;

    while (cert != NULL)
    {
	if (cert->authority_flags & auth_flags
	&& ((keyid.ptr != NULL) ? same_keyid(keyid, cert->subjectKeyID)
	    : (same_dn(subject, cert->subject)
	       && same_serial(serial, cert->serialNumber))))
	{
	    if (cert != x509authcerts)
	    {
		/* bring the certificate up front */
		prev_cert->next = cert->next;
		cert->next = x509authcerts;
		x509authcerts = cert;
	    }
	    return cert;
	}
	prev_cert = cert;
	cert = cert->next;
    }
    return NULL;
}
/*
 *  free the first authority certificate in the chain
 */
static void
free_first_authcert(void)
{
    x509cert_t *first = x509authcerts;
    x509authcerts = first->next;
    free_x509cert(first);
}

/*
 *  free  all CA certificates
 */
void
free_authcerts(void)
{
    lock_authcert_list("free_authcerts");

    while (x509authcerts != NULL)
        free_first_authcert();

    unlock_authcert_list("free_authcerts");
}

/*
 * add an authority certificate to the chained list
 */
void
add_authcert(x509cert_t *cert, u_char auth_flags)
{
    x509cert_t *old_cert;

    /* set authority flags */
    cert->authority_flags |= auth_flags;

    lock_authcert_list("add_authcert");

    old_cert = get_authcert(cert->subject, cert->serialNumber
	, cert->subjectKeyID, auth_flags);

    if (old_cert != NULL)
    {
	if (same_x509cert(cert, old_cert))
	{
	    /* cert is already present, just add additional authority flags */
	    old_cert->authority_flags |= cert->authority_flags;
	    DBG(DBG_X509 | DBG_PARSING ,
		DBG_log("  authcert is already present and identical")
	    )
	    unlock_authcert_list("add_authcert");
	    
	    free_x509cert(cert);
	    return;
	}
	else
	{
	    /* cert is already present but will be replaced by new cert */
	    free_first_authcert();
	    DBG(DBG_X509 | DBG_PARSING ,
		DBG_log("  existing authcert deleted")
	    )
	}
    }
    
    /* add new authcert to chained list */
    cert->next = x509authcerts;
    x509authcerts = cert;
    share_x509cert(cert);  /* set count to one */
    DBG(DBG_X509 | DBG_PARSING,
	DBG_log("  authcert inserted")
    )
    unlock_authcert_list("add_authcert");
}

/*
 *  Loads authority certificates
 */
void
load_authcerts(const char *type, const char *path, u_char auth_flags)
{
    struct dirent **filelist;
    char buf[ASN1_BUF_LEN];
    char *save_dir;
    int n;

    /* change directory to specified path */
    save_dir = getcwd(buf, ASN1_BUF_LEN);

    if (chdir(path))
    {
	openswan_log("Could not change to directory '%s': %s", path, strerror(errno));
    }
    else
    {
	DBG(DBG_CONTROL, DBG_log("Changed path to directory '%s'", path));
	n = scandir(".", &filelist, (void *) file_select, alphasort);

	if (n < 0){
	    char buff[256];
	    strerror_r(errno, buff, 256 );
	    openswan_log("  scandir() ./ error: %s", buff);
	}
	else
	{
	    while (n--)
	    {
		cert_t cert;

		if (load_cert(CERT_NONE, filelist[n]->d_name,
#ifdef SINGLE_CONF_DIR
			FALSE, /* too verbose in single conf dir */
#else
			TRUE,
#endif
			      type, &cert))
		    add_authcert(cert.u.x509, auth_flags);

		free(filelist[n]);
	    }
	    free(filelist);
	}
	
	/* restore directory path */
	if(chdir(save_dir) != 0) {
	    char buff[256];
	    strerror_r(errno, buff, 256 );
	    openswan_log("  chdir() ./ error: %s", buff);
	}
    }
   
}

/********************** auth cert lists **********/

/*
 * Checks if CA a is trusted by CA b
 */
bool
trusted_ca(chunk_t a, chunk_t b, int *pathlen)
{
    bool match = FALSE;
    char abuf[ASN1_BUF_LEN], bbuf[ASN1_BUF_LEN];

    dntoa(abuf, ASN1_BUF_LEN, a);
    dntoa(bbuf, ASN1_BUF_LEN, b);

    DBG(DBG_X509 | DBG_CONTROLMORE
	, DBG_log("  trusted_ca called with a=%s b=%s"
		  , abuf, bbuf));

    /* no CA b specified -> any CA a is accepted */
    if (b.ptr == NULL)
    {
	*pathlen = (a.ptr == NULL)? 0 : MAX_CA_PATH_LEN;
	return TRUE;
    }

    /* no CA a specified -> trust cannot be established */
    if (a.ptr == NULL)
    {
	*pathlen = MAX_CA_PATH_LEN;
	return FALSE;
    }

    *pathlen = 0;

    /* CA a equals CA b -> we have a match */
    if (same_dn(a, b))
	return TRUE;

    /* CA a might be a subordinate CA of b */
    lock_authcert_list("trusted_ca");

    while ((*pathlen)++ < MAX_CA_PATH_LEN)
    {
	x509cert_t *cacert = get_authcert(a, empty_chunk, empty_chunk, AUTH_CA);

	/* cacert not found or self-signed root cacert-> exit */
	if (cacert == NULL || same_dn(cacert->issuer, a))
	    break;

	/* does the issuer of CA a match CA b? */
	match = same_dn(cacert->issuer, b);

	/* we have a match and exit the loop */
	if (match)
	    break;

	/* go one level up in the CA chain */
	a = cacert->issuer;
    }
    
    unlock_authcert_list("trusted_ca");

    DBG(DBG_X509 | DBG_CONTROLMORE
	, DBG_log("  trusted_ca returning with %s", match ? "match" : "failed"));

    return match;
}

/*  Checks if the current certificate is revoked. It goes through the
 *  list of revoked certificates of the corresponding crl. If the
 *  certificate is found in the list, TRUE is returned
 */
bool x509_check_revocation(const x509crl_t *crl, chunk_t serial)
{
    revokedCert_t *revokedCert = crl->revokedCertificates;
    char tbuf[TIMETOA_BUF];

    DBG(DBG_X509,
	DBG_dump_chunk("serial number:", serial)
    )

    while(revokedCert != NULL)
    {
	/* compare serial numbers */
	if (revokedCert->userCertificate.len == serial.len &&
	    memcmp(revokedCert->userCertificate.ptr, serial.ptr, serial.len) == 0)
	{
	    openswan_log("certificate was revoked on %s",
			 timetoa(&revokedCert->revocationDate, TRUE, tbuf, sizeof(tbuf)));
	    return TRUE;
	}
	revokedCert = revokedCert->next;
    }
    DBG(DBG_X509,
	DBG_log("certificate not revoked")
    )
    return FALSE;
}


/*
 * check if any crls are about to expire
 */
void
check_crls(void)
{
#ifdef HAVE_THREADS
    x509crl_t *crl;
    time_t current_time = time(NULL);

    lock_crl_list("check_crls");
    crl = x509crls;

    while (crl != NULL)
    {
	time_t time_left = crl->nextUpdate - current_time;
	u_char buf[ASN1_BUF_LEN];

	DBG(DBG_X509,
	    dntoa(buf, ASN1_BUF_LEN, crl->issuer);
	    DBG_log("issuer: '%s'",buf);
	    if (crl->authKeyID.ptr != NULL)
	    {
		datatot(crl->authKeyID.ptr, crl->authKeyID.len, ':'
		    , buf, ASN1_BUF_LEN);
		DBG_log("authkey: %s", buf);
	    }
	    DBG_log("%ld seconds left", time_left)
	)
	if (time_left < 2*crl_check_interval)
	    add_crl_fetch_request(crl->issuer, crl->distributionPoints);
	crl = crl->next;
    }
    unlock_crl_list("check_crls");
#endif
}

/*
 * get a cacert with a given subject or keyid from an alternative list
 */
static const x509cert_t*
get_alt_cacert(chunk_t subject, chunk_t serial, chunk_t keyid
    , const x509cert_t *cert)
{
    while (cert != NULL)
    {
       if ((keyid.ptr != NULL) ? same_keyid(keyid, cert->subjectKeyID)
           : (same_dn(subject, cert->subject)
              && same_serial(serial, cert->serialNumber)))
       {
           return cert;
       }
       cert = cert->next;
    }
    return NULL;
}


/* establish trust into a candidate authcert by going up the trust chain.
 * validity and revocation status are not checked.
 */
bool
trust_authcert_candidate(const x509cert_t *cert, const x509cert_t *alt_chain)
{
    int pathlen;

    lock_authcert_list("trust_authcert_candidate");

    for (pathlen = 0; pathlen < MAX_CA_PATH_LEN; pathlen++)
    {
       const x509cert_t *authcert = NULL;

       DBG(DBG_CONTROL,
           char buf[ASN1_BUF_LEN];
           dntoa(buf, ASN1_BUF_LEN, cert->subject);
           DBG_log("subject: '%s'",buf);
           dntoa(buf, ASN1_BUF_LEN, cert->issuer);
           DBG_log("issuer:  '%s'",buf);
           if (cert->authKeyID.ptr != NULL)
           {
               datatot(cert->authKeyID.ptr, cert->authKeyID.len, ':'
                   , buf, ASN1_BUF_LEN);
               DBG_log("authkey:  %s", buf);
           }
	   );

       /* search in alternative chain first */
       authcert = get_alt_cacert(cert->issuer, cert->authKeySerialNumber
           , cert->authKeyID, alt_chain);

       if (authcert != NULL)
       {
           DBG(DBG_CONTROL,
               DBG_log("issuer cacert found in alternative chain")
           )
       }
       else
       {
           /* search in trusted chain */
           authcert = get_authcert(cert->issuer, cert->authKeySerialNumber
               , cert->authKeyID, AUTH_CA);

           if (authcert != NULL)
           {
               DBG(DBG_CONTROL,
                   DBG_log("issuer cacert found")
               )
           }
           else
           {
               plog("issuer cacert not found");
               unlock_authcert_list("trust_authcert_candidate");
               return FALSE;
           }
       }

       if (!check_signature(cert->tbsCertificate, cert->signature,
                            cert->algorithm, authcert))
       {
           plog("invalid certificate signature");
           unlock_authcert_list("trust_authcert_candidate");
           return FALSE;
       }
       DBG(DBG_CONTROL,
           DBG_log("valid certificate signature")
       )

       /* check if cert is a self-signed root ca */
       if (pathlen > 0 && same_dn(cert->issuer, cert->subject))
       {
           DBG(DBG_CONTROL,
               DBG_log("reached self-signed root ca")
           )
           unlock_authcert_list("trust_authcert_candidate");
           return TRUE;
       }

       /* go up one step in the trust chain */
       cert = authcert;
    }
    plog("maximum ca path length of %d levels exceeded", MAX_CA_PATH_LEN);
    unlock_authcert_list("trust_authcert_candidate");
    return FALSE;
}

/* verify the validity of a certificate by
 * checking the notBefore and notAfter dates
 */
err_t
check_validity(const x509cert_t *cert, time_t *until)
{
    time_t current_time;
    char curtime[TIMETOA_BUF];

    time(&current_time);
    timetoa(&current_time, TRUE, curtime, sizeof(curtime));

    DBG(DBG_X509,
	char tbuf[TIMETOA_BUF];
	
	DBG_log("  not before  : %s"
		, timetoa(&cert->notBefore, TRUE, tbuf, sizeof(tbuf)));
	DBG_log("  current time: %s", curtime);
	DBG_log("  not after   : %s"
		, timetoa(&cert->notAfter, TRUE, tbuf, sizeof(tbuf)));
	);

    if (cert->notAfter < *until) *until = cert->notAfter;

    if (current_time < cert->notBefore) {
	char tbuf[TIMETOA_BUF];

	return builddiag("X.509 certificate is not valid until %s (it is now=%s)"
			 , timetoa(&cert->notBefore, TRUE, tbuf, sizeof(tbuf))
			 , curtime);
    }
    
    if (current_time > cert->notAfter) {
	char tbuf[TIMETOA_BUF];

	DBG_log("  aftercheck : %ld > %ld", (unsigned long)current_time
		, (unsigned long)cert->notAfter);
	return builddiag("X.509 certificate expired at %s (it is now %s)"
			 , timetoa(&cert->notAfter, TRUE, tbuf, sizeof(tbuf))
			 , curtime);
    }

    else
	return NULL;
}

/* 
 * does our CA match one of the requested CAs?
 */
bool
match_requested_ca(generalName_t *requested_ca, chunk_t our_ca, int *our_pathlen)
{
    /* if no ca is requested than any ca will match */
    if (requested_ca == NULL)
    {
	*our_pathlen = 0;
	return TRUE;
    }

    *our_pathlen = MAX_CA_PATH_LEN + 1;

    while (requested_ca != NULL)
    {
	int pathlen;

	if (trusted_ca(our_ca, requested_ca->name, &pathlen)
	&& pathlen < *our_pathlen)
	    *our_pathlen = pathlen;
	requested_ca = requested_ca->next;
    }

    return *our_pathlen <= MAX_CA_PATH_LEN;
}

/*
 * Local Variables:
 * c-basic-offset:4
 * c-style: pluto
 * End:
 */

