/*
    See license.txt in the root of this project.
*/

# include "luametatex.h"
# include "lmtoptional.h"

/*
    This optional is not yet officially supported and mostly serves as playground. One problem with
    certificates like this is that one cannot use letsencrypt (and similar free) certificates but
    has to go for expensive, vendor locked in ones. Also, in viewers like acribat root certificates
    are compiled in so ... in the end it's all not that useful in pdf in the perspective of open
    and free software.

    It tooks a bit of work to get the (in the end not that much) code because searching online gives
    confusing and even wrong results. So in the end it came down to looking into openssl and a bit of
    trial and error. Therefore the following code is likely to evolve.
*/

typedef void * BIO;
typedef void * EVP_PKEY;
typedef void * X509;
typedef void * X509_STORE;
typedef void * pem_password_cb;
typedef void * PKCS7;
typedef void * OPENSSL_STACK;

typedef struct openssllib_state_info {

    int initialized;
    int padding;

    void (*OPENSSL_init_ssl) (
        int,
        void *
    );

    PKCS7 * (*PKCS7_sign) (
        X509          *signcert,
        EVP_PKEY      *pkey,
        X509         **certs,
        BIO           *data,
        unsigned int   flags
    );

    int (*PKCS7_final) (
        PKCS7 *p7,
        BIO   *data,
        int    flags
    );

    int (*i2d_PKCS7) (
        PKCS7          *a,
        unsigned char **ppout
    );

    int (*i2d_PKCS7_DIGEST) (
        PKCS7          *a,
        unsigned char **ppout
    );

    int (*i2d_PKCS7_bio) (
        BIO   *bp,
        PKCS7 *a
    );

    int (*SMIME_write_PKCS7) (
        BIO   *out,
        PKCS7 *p7,
        BIO   *data,
        int    flags
    );

    void (*PKCS7_free) (
        PKCS7 *
    );

    void (*X509_free) (
        X509 *
    );

    void (*EVP_PKEY_free) (
        EVP_PKEY *
    );

    void * (*BIO_new_file) (
        const char *filename,
        const char *mode
    );

    void * (*BIO_new_mem_buf) (
        const void *data,
        int         len
    );

    long (*BIO_ctrl) (
        BIO  *bp,
        int   cmd,
        long  larg,
        void *parg
    );

    void (*BIO_free) (
        BIO *
    );

    X509 * (*PEM_read_bio_X509) (
        BIO             *bp,
        X509            **x,
        pem_password_cb *cb,
        const char       *u
    );

    EVP_PKEY * (*PEM_read_bio_PrivateKey) (
        BIO              *bp,
        EVP_PKEY        **x,
        pem_password_cb  *cb,
        const char       *u
    );

    int (*PKCS7_verify) (
        PKCS7      *p7,
        X509       *certs, // stack
        X509_STORE *store,
        BIO        *indata,
        BIO        *out,
        int         flags
    );

    PKCS7 * (*d2i_PKCS7) (
        PKCS7               **a,
        const unsigned char **ppin,
        long                  length
    );

    PKCS7 * (*d2i_PKCS7_DIGEST) (
        PKCS7               **a,
        const unsigned char **ppin,
        long                  length
    );

    void (*CRYPTO_free) (
        void       *ptr,
        const char *file,
        int         line
    );

} openssllib_state_info;

# define BIO_CTRL_RESET  1 /* opt - rewind/zero etc */
# define BIO_CTRL_INFO   3 /* opt - extra tit-bits */

# define PKCS7_DETACHED 0x0040
# define PKCS7_BINARY   0x0080
# define PKCS7_STREAM   0x1000

# define PKCS7_NOINTERN 0x0010
# define PKCS7_NOVERIFY 0x0020

# define SSL_library_init()     openssllib_state.OPENSSL_init_ssl(0, NULL)
# define BIO_reset(b)           openssllib_state.BIO_ctrl(b,BIO_CTRL_RESET,0,NULL)
# define BIO_get_mem_data(b,pp) openssllib_state.BIO_ctrl(b,BIO_CTRL_INFO,0,(char *)(pp))

static openssllib_state_info openssllib_state = {

    .initialized             = 0,
    .padding                 = 0,

    .OPENSSL_init_ssl        = NULL,
    .PKCS7_sign              = NULL,
    .PKCS7_final             = NULL,
    .i2d_PKCS7               = NULL,
    .i2d_PKCS7_bio           = NULL,
    .PKCS7_free              = NULL,
    .X509_free               = NULL,
    .EVP_PKEY_free           = NULL,
    .BIO_new_file            = NULL,
    .BIO_new_mem_buf         = NULL,
    .BIO_ctrl                = NULL,
    .BIO_free                = NULL,
    .PEM_read_bio_X509       = NULL,
    .PEM_read_bio_PrivateKey = NULL,
    .PKCS7_verify            = NULL,
    .d2i_PKCS7               = NULL,
    .d2i_PKCS7_DIGEST        = NULL,
    .CRYPTO_free             = NULL,

};

static int openssllib_initialize(lua_State * L)
{
    if (! openssllib_state.initialized) {
        const char *filename_c = lua_tostring(L, 1);
        const char *filename_s = lua_tostring(L, 2);
        if (filename_c && filename_s) {

            lmt_library lib_c = lmt_library_load(filename_c);
            lmt_library lib_s = lmt_library_load(filename_s);

            openssllib_state.OPENSSL_init_ssl        = lmt_library_find(lib_s, "OPENSSL_init_ssl");

            openssllib_state.PKCS7_sign              = lmt_library_find(lib_c, "PKCS7_sign");
            openssllib_state.PKCS7_final             = lmt_library_find(lib_c, "PKCS7_final");
            openssllib_state.i2d_PKCS7               = lmt_library_find(lib_c, "i2d_PKCS7");
            openssllib_state.i2d_PKCS7_bio           = lmt_library_find(lib_c, "i2d_PKCS7_bio");
            openssllib_state.PKCS7_free              = lmt_library_find(lib_c, "PKCS7_free");

            openssllib_state.X509_free               = lmt_library_find(lib_c, "X509_free");
            openssllib_state.EVP_PKEY_free           = lmt_library_find(lib_c, "EVP_PKEY_free");
            openssllib_state.BIO_new_file            = lmt_library_find(lib_c, "BIO_new_file");
            openssllib_state.BIO_new_mem_buf         = lmt_library_find(lib_c, "BIO_new_mem_buf");
            openssllib_state.BIO_ctrl                = lmt_library_find(lib_c, "BIO_ctrl");
            openssllib_state.BIO_free                = lmt_library_find(lib_c, "BIO_free");
            openssllib_state.PEM_read_bio_X509       = lmt_library_find(lib_c, "PEM_read_bio_X509");
            openssllib_state.PEM_read_bio_PrivateKey = lmt_library_find(lib_c, "PEM_read_bio_PrivateKey");

            openssllib_state.PKCS7_verify            = lmt_library_find(lib_c, "PKCS7_verify");
            openssllib_state.d2i_PKCS7               = lmt_library_find(lib_c, "d2i_PKCS7");
            openssllib_state.d2i_PKCS7_DIGEST        = lmt_library_find(lib_c, "d2i_PKCS7_DIGEST");
            openssllib_state.CRYPTO_free             = lmt_library_find(lib_c, "CRYPTO_free");

            openssllib_state.initialized = lmt_library_okay(lib_s) && lmt_library_okay(lib_c);

            if (openssllib_state.OPENSSL_init_ssl) {
                openssllib_state.OPENSSL_init_ssl(0, NULL);
            }
        }
    }
    lua_pushboolean(L, openssllib_state.initialized);
    return 1;
}

/*tex
    Let's save some bytes and delegate the error strings to the \LUA\ module. 
*/

typedef enum messages {
    m_all_is_okay,
    m_invalid_certificate_file,
    m_invalid_certificate,
    m_invalid_private_key,
    m_invalid_data_file,
    m_invalid_signature,
    m_unable_to_open_output_file,
    m_unable_to_reset_file,
    m_unable_to_save_signature,
    m_incomplete_specification,
    m_library_is_unitialized,
} messages;

/* 
    We could zero the password but who knows what is kept in memory by openssl. We just 
    assume a safe server. 
*/

static int openssllib_sign(lua_State * L)
{
    int message = m_all_is_okay;
    if (openssllib_state.initialized) {
        if (lua_type(L, 1) == LUA_TTABLE) {
            const char    *certfile   = NULL;
            const char    *datafile   = NULL;
            const char    *password   = NULL;
            const char    *resultfile = NULL;
            const char    *data       = NULL;
            unsigned char *resultdata = NULL;
            size_t datasize = 0;
            int resultsize = 0;
            if (lua_getfield(L, 1, "certfile")   == LUA_TSTRING) { certfile   = lua_tostring (L, -1);            } lua_pop(L, 1);
            if (lua_getfield(L, 1, "datafile")   == LUA_TSTRING) { datafile   = lua_tostring (L, -1);            } lua_pop(L, 1);
            if (lua_getfield(L, 1, "data")       == LUA_TSTRING) { data       = lua_tolstring(L, -1, &datasize); } lua_pop(L, 1);
            if (lua_getfield(L, 1, "password")   == LUA_TSTRING) { password   = lua_tostring (L, -1);            } lua_pop(L, 1);
            if (lua_getfield(L, 1, "resultfile") == LUA_TSTRING) { resultfile = lua_tostring (L, -1);            } lua_pop(L, 1);
            if (certfile && password && (data || datafile)) {
                int       okay        = 0;
                BIO      *input       = NULL;
                BIO      *output      = NULL;
                X509     *x509        = NULL;
                EVP_PKEY *key         = NULL;
                PKCS7    *p7          = NULL;
                int       flags       = PKCS7_DETACHED | PKCS7_STREAM | PKCS7_BINARY;
                BIO      *certificate = openssllib_state.BIO_new_file(certfile, "rb");
                if (! certificate) {
                    message = m_invalid_certificate_file;
                    goto DONE;
                }
                x509 = openssllib_state.PEM_read_bio_X509(certificate, NULL, NULL, (void *) (uintptr_t) password);
                if (! x509) {
                    message = m_invalid_certificate;
                    goto DONE;
                }
                if (BIO_reset(certificate) < 0) {
                    message = m_unable_to_reset_file;
                    goto DONE;
                }
                key = openssllib_state.PEM_read_bio_PrivateKey(certificate, NULL, NULL, (void *) (uintptr_t) password);
                if (! key) {
                    message = m_invalid_private_key;
                    goto DONE;
                }
                if (datafile) {
                    input = openssllib_state.BIO_new_file(datafile, "rb");
                } else {
                    input = openssllib_state.BIO_new_mem_buf(data, (int) datasize);
                }
                if (! input) {
                    message = m_invalid_data_file;
                    goto DONE;
                }
                p7 = openssllib_state.PKCS7_sign(x509, key, NULL, input, flags);
                if (! p7) {
                    message = m_invalid_signature;
                    goto DONE;
                }
                openssllib_state.PKCS7_final(p7, input, flags);
                if (resultfile) {
                    output = openssllib_state.BIO_new_file(resultfile, "wb");
                    if (! output) {
                        message = m_unable_to_open_output_file;
                        goto DONE;
                    }
                    if (! openssllib_state.i2d_PKCS7_bio(output, p7)) {
                        message = m_unable_to_save_signature;
                        goto DONE;
                    }
                } else {
                    resultsize = openssllib_state.i2d_PKCS7(p7, &resultdata);
                }
                okay = 1;
              DONE:
                if (certificate) { openssllib_state.BIO_free(certificate); }
                if (x509)        { openssllib_state.X509_free(x509);       }
                if (p7)          { openssllib_state.PKCS7_free(p7);        }
                if (key)         { openssllib_state.EVP_PKEY_free(key);    }
                if (input)       { openssllib_state.BIO_free(input);       }
                if (output)      { openssllib_state.BIO_free(output);      }
                if (okay) {
                    lua_pushboolean(L, 1);
                    if (resultdata && resultsize > 0) {
                        lua_pushlstring(L, (const char *) resultdata, (size_t) resultsize);
                        if (openssllib_state.CRYPTO_free) {
                            openssllib_state.CRYPTO_free(resultdata, __FILE__, __LINE__);
                        } else {
                            free(resultdata);
                        }
                        return 2;
                    } else {
                        return 1;
                    }
                }
            }
        } else {
            message = m_incomplete_specification;
        }
    } else {
        message = m_library_is_unitialized;
    }
    lua_pushboolean(L, 0);
    if (message) {
        lua_pushinteger(L, message);
        return 2;
    } else {
        return 1;
    }
}

static int openssllib_verify(lua_State * L)
{
    int message = m_all_is_okay;
    if (openssllib_state.initialized) {
        const char *certfile      = NULL;
        const char *datafile      = NULL;
        const char *password      = NULL;
        const char *signature     = NULL;
        const char *data          = NULL;
        size_t      signaturesize = 0;
        size_t      datasize      = 0;
        if (lua_getfield(L, 1, "certfile")  == LUA_TSTRING) { certfile  = lua_tostring (L, -1);                 } lua_pop(L, 1);
        if (lua_getfield(L, 1, "datafile")  == LUA_TSTRING) { datafile  = lua_tostring (L, -1);                 } lua_pop(L, 1);
        if (lua_getfield(L, 1, "data")      == LUA_TSTRING) { data      = lua_tolstring(L, -1, &datasize);      } lua_pop(L, 1);
        if (lua_getfield(L, 1, "signature") == LUA_TSTRING) { signature = lua_tolstring(L, -1, &signaturesize); } lua_pop(L, 1);
        if (lua_getfield(L, 1, "password")  == LUA_TSTRING) { password  = lua_tostring (L, -1);                 } lua_pop(L, 1);
        if (certfile && password && (data || datafile) && signature) {
            int    okay        = -1;
            PKCS7 *p7          = NULL;
            BIO   *input       = NULL;
            X509  *x509        = NULL;
            int    flags       = PKCS7_NOVERIFY | PKCS7_BINARY;
            BIO   *certificate = openssllib_state.BIO_new_file(certfile, "rb");
            if (! certificate) {
                message = m_invalid_certificate_file;
                goto DONE;
            }
            x509 = openssllib_state.PEM_read_bio_X509(certificate, NULL, NULL, (void *) (uintptr_t) password);
            if (! x509) {
                message = m_invalid_certificate;
                goto DONE;
            }
            const unsigned char *p = (const unsigned char *) signature;
            p7 = openssllib_state.d2i_PKCS7(NULL, &p, (long) signaturesize);
            if (! p7) {
                message = m_invalid_signature;
                goto DONE;
            }
            if (datafile) {
                input = openssllib_state.BIO_new_file(datafile, "rb");
            } else {
                input = openssllib_state.BIO_new_mem_buf(data, (int) datasize);
            }
            if (! input) {
                message = m_invalid_data_file;
                goto DONE;
            }
            okay = openssllib_state.PKCS7_verify(p7, NULL, NULL, input, NULL, flags);
          DONE:
            if (certificate) { openssllib_state.BIO_free(certificate);  }
            if (x509)        { openssllib_state.X509_free(x509);        }
            if (p7)          { openssllib_state.PKCS7_free(p7);         }
            if (input)       { openssllib_state.BIO_free(input);        }
            if (okay > 0) {
                lua_pushboolean(L, okay);
                return 1;
            } else {
                message = m_invalid_signature;
            }
        }
    }
    lua_pushboolean(L, 0);
    if (message) {
        lua_pushinteger(L, message);
        return 2;
    } else {
        return 1;
    }
}

static int openssllib_getversion(lua_State * L)
{
    if (openssllib_state.initialized) {
        lua_pushinteger(L, 1);
    } else {
        lua_pushinteger(L, 0);
    }
    return 1;
}

static struct luaL_Reg openssllib_function_list[] = {
    { "initialize", openssllib_initialize },
    { "sign",       openssllib_sign       },
    { "verify",     openssllib_verify     },
    { "getversion", openssllib_getversion },
    { NULL,         NULL                  },
};

int luaopen_openssl(lua_State * L)
{
    lmt_library_register(L, "openssl", openssllib_function_list);
    return 0;
}
