/* parse_certificate_files.c  --  Parses PEM or PKCS12 formatted cert. files
 *
 *  GPLv2 only - Copyright (C) 2008 - 2012
 *               David Sommerseth <dazo@users.sourceforge.net>
 *
 *  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; version 2
 *  of the License.
 *
 *  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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */

/**
 * @file   parse_certificate_files.c
 * @author David Sommerseth <dazo@users.sourceforge.net>
 * @date   2008-12-21
 *
 * @brief  Parses PEM/DER or PKCS#12 certificate files to retrieve information
 *         needed by eurephia.  This feature requires OpenSSL to be available.
 *
 */

#ifdef HAVE_OPENSSL
#include <stdio.h>
#include <string.h>
#include <assert.h>

#include <openssl/ssl.h>
#include <openssl/pkcs12.h>
#include <openssl/evp.h>
#include <openssl/err.h>

#include <eurephia_nullsafe.h>
#include <certinfo.h>

#include "get_console_input.h"
#define _PARSE_CERTFICIATE_FILES_C
#include "parse_certificate_files.h"


/**
 * Extracts a specific X.509 field from an X509 struct pointer.
 *
 * @param module     String containing a module name, only used in error situations
 * @param cert       Pointer to an X509 struct with the certificate information
 * @param fieldname  X.509 field name to extract
 *
 * @return Returns a string (char *) to the field value if found, otherwise NULL.
 */
char *ExtractCertInfo(const char *module, X509 *cert, const char *fieldname) {
        unsigned char *buf = (unsigned char *)1;  // Needs to be 1 to avoid OpenSSL 0.9.6b bug
        char resbuf[2050];
        X509_NAME *name = NULL;
        X509_NAME_ENTRY *namentry = NULL;
        ASN1_STRING *asn1 = NULL;
        int nid, tmp = -1, pos = -1;

        //
        // Extract subject information
        //

        memset(resbuf, 0, 2050);

        nid = OBJ_txt2nid(fieldname);
        name = X509_get_subject_name(cert);

        do {
                pos = tmp;
                tmp = X509_NAME_get_index_by_NID(name, nid, pos);
        } while ( tmp > -1 );

        if( pos == -1 ) {
                fprintf(stderr, "%s: Field '%s' not found\n", module, fieldname);
                return 0;
        }

        if( !(namentry = X509_NAME_get_entry(name, pos)) ) {
                fprintf(stderr, "%s: Failed to extract name entry from field '%s'\n", module, fieldname);
                return 0;
        }

        if( !(asn1 = X509_NAME_ENTRY_get_data(namentry)) ) {
                fprintf(stderr, "%s: Failed to extract data from name entry field '%s'\n", module, fieldname);
                return 0;
        }

        if( ASN1_STRING_to_UTF8(&buf, asn1) <= 0 ) {
                fprintf(stderr, "%s: Failed to convert ASN1 string to UTF-8 for '%s'\n", module, fieldname);
                return 0;
        }

        snprintf(resbuf, 2048, "%s%c", buf, '\0');
        OPENSSL_free(buf);

        return strdup_nullsafe(resbuf);
}


/**
 * Internal function, usually called via the Cert_ParseFile(...) macro.  Parses a certificate
 * file of a given format.
 *
 * @param module          String containing a module name, only used in error situations
 * @param certfile        File name of the input file
 * @param certfile_format Certificate format of the file
 *
 * @return Returns a certinfo struct pointer with the parsed result on success, otherwise NULL.
 */
certinfo *_Cert_ParseFile(const char *module, const char *certfile, int certfile_format) {
        BIO *bio_err = NULL;
        PKCS12 *p12 = NULL;
        EVP_PKEY *pkey = NULL;
        X509 *cert = NULL;
        FILE *fp;
        certinfo *ret = NULL;

        /* Needed to convert X509 digest into hex string */
        unsigned char md_sha1[EVP_MAX_MD_SIZE];
        unsigned int mdlen;

        if( !bio_err ) {
                SSL_library_init();
                SSL_load_error_strings();
                bio_err = BIO_new_fp(stderr, BIO_NOCLOSE);
        }

        // Open file - according to defined format
        switch( certfile_format ) {
        case CERTFILE_PEM: // PEM/DER format
                fp = fopen(certfile, "r");
                if( !(cert = PEM_read_X509(fp, NULL, NULL, NULL)) ) {
                        fprintf(stderr, "%s: Failed to open certificate file\n", module);
                        return NULL;
                }
                fclose(fp);
                break;

        case CERTFILE_PKCS12: // PKCS#12 format
                fp = fopen(certfile, "r");
                p12 = d2i_PKCS12_fp(fp, NULL);
                fclose(fp);
                if( p12 == NULL ) {
                        fprintf(stderr, "%s: Could not open PKCS#12 file\n", module);
                        return NULL;
                }
                OpenSSL_add_all_ciphers();

                // First, try without password
                if( !PKCS12_parse(p12, "", &pkey, &cert, NULL) ) {
                        char pwd[130];

                        // If empty password failed, get password and try again
                        memset(&pwd, 0, 130);
                        if( get_console_input(pwd, 128, "PKCS12 password:", 1) < 0 ) {
                                fprintf(stderr, "Could not retrieve password\n");
                        }
                        if( !PKCS12_parse(p12, pwd, &pkey, &cert, NULL) ) {
                                PKCS12_free(p12); p12 = NULL;
                                fprintf(stderr,
                                        "%s: Could not open PKCS#12 file - wrong password\n", module);
                                fprintf(stderr,
                                        "%s: %s\n", module, ERR_error_string(ERR_get_error(), NULL));
                                BIO_free(bio_err);
                                return NULL;
                        }
                }
                EVP_PKEY_free(pkey); pkey = NULL;
                PKCS12_free(p12); p12 = NULL;
                break;

        default: // Unknown
                fprintf(stderr, "%s: Unknown certificate file format\n", module);
                return NULL;
        }

        ret = (certinfo *) malloc_nullsafe(NULL, sizeof(certinfo)+2);
        assert( ret != NULL );

        ret->digest = (char *) malloc_nullsafe(NULL, 66);
        assert(ret != NULL);


        // extract SHA1 digest from certificate
        if (X509_digest(cert, EVP_sha1(), md_sha1, &mdlen) && mdlen > 0) {
                static const char hexcodes[] = "0123456789ABCDEF";
                int j;

                for (j = 0; j < (int) mdlen; j++) {
                        ret->digest[j * 3] = hexcodes[(md_sha1[j] & 0xf0) >> 4U];
                        ret->digest[(j * 3) + 1] = hexcodes[(md_sha1[j] & 0x0f)];
                        if (j + 1 != (int) mdlen) {
                                ret->digest[(j * 3) + 2] = ':';
                        } else {
                                ret->digest[(j * 3) + 2] = '\0';
                        }
                }
        }

        // Extract the subject information we want
        ret->common_name = ExtractCertInfo(module, cert, "CN");
        ret->org         = ExtractCertInfo(module, cert, "O");
        ret->email       = ExtractCertInfo(module, cert, "emailAddress");

        X509_free(cert);
        BIO_free(bio_err);

        return ret;
}
#endif
