module OpenSSL

using BitFlags
using Dates
using OpenSSL_jll
using Sockets
using MozillaCACerts_jll
using NetworkOptions: NetworkOptions

"""
    [x] Encryption, decryption
    [x] X509 extension
    [x] Free BIO
    [x] Free BIOMethod
    [x] Close SSLContext
    [x] BIOStream method (callbacks)
    [x] Store the SSLContext (part of SSLStream)
    [ ] DSA certificates
    [ ] encryption/decryption set key length

Error handling:
    OpenSSL keeps the error messages in the thread TLS.
    After ccall to OpenSSL, before task has an oportinity to yield,
    we clear the OpenSSL error queue, and store the error messages in Julia task TLS.
"""

export TLSv12ClientMethod, TLSv12ServerMethod,
    SSLStream, BigNum, EvpPKey, RSA, DSA, Asn1Time, X509Name, StackOf, X509Certificate,
    X509Request, X509Store, X509Attribute, X509Extension, P12Object, EvpDigestContext, EvpCipherContext,
    EvpEncNull, EvpBlowFishCBC, EvpBlowFishECB, EvpBlowFishCFB, EvpBlowFishOFB, EvpAES128CBC,
    EvpAES128ECB, EvpAES128CFB, EvpAES128OFB, EvpMDNull, EvpMD2, EvpMD5, EvpSHA1, EvpDSS1,
    encrypt_init, cipher, add_extension, add_extensions, decrypt_init, digest_init, digest_update, digest_final,
    digest, random_bytes, rsa_generate_key, dsa_generate_key, add_entry, sign_certificate, sign_request, adjust,
    add_cert, unpack, eof, isreadable, iswritable, bytesavailable, read, unsafe_write, connect,
    get_peer_certificate, free, HTTP2_ALPN, UPDATE_HTTP2_ALPN, version

const Option{T} = Union{Nothing,T} where {T}

const HTTP2_ALPN = "\x02h2"
const UPDATE_HTTP2_ALPN = "\x02h2\x08http/1.1"

"""
    These are used in the following macros and are passed to BIO_ctrl().
"""
@enum(BIOCtrl::Cint,
    # opt - rewind/zero etc.
    BIO_CTRL_RESET = 1,
    # opt - are we at the eof.
    BIO_CTRL_EOF = 2,
    # opt - extra tit-bits.
    BIO_CTRL_INFO = 3,
    # man - set the 'IO' type.
    BIO_CTRL_SET = 4,
    # man - set the 'IO' type.
    BIO_CTRL_GET = 5,
    # opt - internal, used to signify change.
    BIO_CTRL_PUSH = 6,
    # opt - internal, used to signify change.
    BIO_CTRL_POP = 7,
    # man - set the 'close' on free.
    BIO_CTRL_GET_CLOSE = 8,
    # man - set the 'close' on free.
    BIO_CTRL_SET_CLOSE = 9,
    # opt - is their more data buffered.
    BIO_CTRL_PENDING = 10,
    # opt - 'flush' buffered output.
    BIO_CTRL_FLUSH = 11,
    # man - extra stuff for 'duped' BIO
    BIO_CTRL_DUP = 12,
    # opt - number of bytes still to writes
    BIO_CTRL_WPENDING = 13,
    # opt - set callback function
    BIO_CTRL_SET_CALLBACK = 14,
    # opt - set callback function
    BIO_CTRL_GET_CALLBACK = 15,
    # BIO_f_buffer special
    BIO_CTRL_PEEK = 29,
    # BIO_s_file special,
    BIO_CTRL_SET_FILENAME = 30,
    # dgram BIO stuff:
    # BIO_s_file special.
    BIO_CTRL_DGRAM_CONNECT = 31,
    # allow for an externally connected socket to be passed in.
    BIO_CTRL_DGRAM_SET_CONNECTED = 32,
    # setsockopt, essentially.
    BIO_CTRL_DGRAM_SET_RECV_TIMEOUT = 33,
    # getsockopt, essentially.
    BIO_CTRL_DGRAM_GET_RECV_TIMEOUT = 34,
    # setsockopt, essentially.
    BIO_CTRL_DGRAM_SET_SEND_TIMEOUT = 35,
    # getsockopt, essentially
    BIO_CTRL_DGRAM_GET_SEND_TIMEOUT = 36,
    # flag whether the last.
    BIO_CTRL_DGRAM_GET_RECV_TIMER_EXP = 37,
    # I/O operation tiemd out.
    BIO_CTRL_DGRAM_GET_SEND_TIMER_EXP = 38,
    # set DF bit on egress packets
    BIO_CTRL_DGRAM_MTU_DISCOVER = 39,
    # as kernel for current MTU,
    BIO_CTRL_DGRAM_QUERY_MTU = 40, BIO_CTRL_DGRAM_GET_FALLBACK_MTU = 47,
    # get cached value for MTU.
    BIO_CTRL_DGRAM_GET_MTU = 41,
    # set cached value for MTU. Want to use this if asking the kernel fails.
    BIO_CTRL_DGRAM_SET_MTU = 42,
    # check whether the MTU was exceed in the previous write operation.
    BIO_CTRL_DGRAM_MTU_EXCEEDED = 43, BIO_CTRL_DGRAM_GET_PEER = 46,
    # Destination for the data.
    BIO_CTRL_DGRAM_SET_PEER = 44,
    # Next DTLS handshake timeout to adjust socket timeouts.
    BIO_CTRL_DGRAM_SET_NEXT_TIMEOUT = 45,
    # Do not fragment bit for the current socket, if possible on the platform.
    BIO_CTRL_DGRAM_SET_DONT_FRAG = 48,
    #
    BIO_CTRL_DGRAM_GET_MTU_OVERHEAD = 49,
    # SCTP stuff
    BIO_CTRL_DGRAM_SCTP_SET_IN_HANDSHAKE = 50,
    #
    BIO_CTRL_DGRAM_SCTP_ADD_AUTH_KEY = 51,
    #
    BIO_CTRL_DGRAM_SCTP_NEXT_AUTH_KEY = 52,
    #
    BIO_CTRL_DGRAM_SCTP_AUTH_CCS_RCVD = 53,
    #
    BIO_CTRL_DGRAM_SCTP_GET_SNDINFO = 60,
    #
    BIO_CTRL_DGRAM_SCTP_SET_SNDINFO = 61,
    #
    BIO_CTRL_DGRAM_SCTP_GET_RCVINFO = 62,
    #
    BIO_CTRL_DGRAM_SCTP_SET_RCVINFO = 63,
    #
    BIO_CTRL_DGRAM_SCTP_GET_PRINFO = 64,
    #
    BIO_CTRL_DGRAM_SCTP_SET_PRINFO = 65,
    #
    BIO_CTRL_DGRAM_SCTP_SAVE_SHUTDOWN = 70,
    # Set peek mode.
    BIO_CTRL_DGRAM_SET_PEEK_MODE = 71,
    # BIO_get_ktls_send() returns 1 if the BIO is using the Kernel TLS data-path for sending.
    BIO_CTRL_GET_KTLS_SEND = 73,
    # BIO_get_ktls_recv() returns 1 if the BIO is using the Kernel TLS data-path for receiving.
    BIO_CTRL_GET_KTLS_RECV = 76,
    #
    BIO_CTRL_DGRAM_SCTP_WAIT_FOR_DRY = 77,
    #
    BIO_CTRL_DGRAM_SCTP_MSG_WAITING = 78,
    # BIO_f_prefix controls.
    # BIO_set_prefix() sets the prefix to be used for future lines of text.
    BIO_CTRL_SET_PREFIX = 79,
    # BIO_set_indent() sets the indentation to be used for future lines of text, using indent.
    BIO_CTRL_SET_INDENT = 80,
    # BIO_get_indent() gets the current indentation.
    BIO_CTRL_GET_INDENT = 81)

"""
    Classes of BIOs.
"""
# Socket, fd, connect or accept.
const BIO_TYPE_DESCRIPTOR = 0x0100
const BIO_TYPE_FILTER = 0x0200
const BIO_TYPE_SOURCE_SINK = 0x0400

"""
    BIO types.
"""
@enum(BIOType::Cint,
    #
    BIO_TYPE_NONE = 0,
    #
    BIO_TYPE_MEM = 1 | BIO_TYPE_SOURCE_SINK,
    #
    BIO_TYPE_FILE = 2 | BIO_TYPE_SOURCE_SINK,
    #
    BIO_TYPE_FD = 4 | BIO_TYPE_SOURCE_SINK | BIO_TYPE_DESCRIPTOR,
    #
    BIO_TYPE_SOCKET = 5 | BIO_TYPE_SOURCE_SINK | BIO_TYPE_DESCRIPTOR,
    #
    BIO_TYPE_NULL = 6 | BIO_TYPE_SOURCE_SINK,
    #
    BIO_TYPE_SSL = 7 | BIO_TYPE_FILTER,
    # Message digest BIO filter.
    BIO_TYPE_MD = 8 | BIO_TYPE_FILTER,
    #
    BIO_TYPE_BUFFER = 9 | BIO_TYPE_FILTER,
    #
    BIO_TYPE_CIPHER = 10 | BIO_TYPE_FILTER,
    #
    BIO_TYPE_BASE64 = 11 | BIO_TYPE_FILTER,
    #
    BIO_TYPE_CONNECT = 12 | BIO_TYPE_SOURCE_SINK | BIO_TYPE_DESCRIPTOR,
    #
    BIO_TYPE_ACCEPT = 13 | BIO_TYPE_SOURCE_SINK | BIO_TYPE_DESCRIPTOR,
    # server proxy BIO
    BIO_TYPE_NBIO_TEST = 16 | BIO_TYPE_FILTER,
    #
    BIO_TYPE_NULL_FILTER = 17 | BIO_TYPE_FILTER,
    # Half a BIO pair.
    BIO_TYPE_BIO = 19 | BIO_TYPE_SOURCE_SINK,
    #
    BIO_TYPE_LINEBUFFER = 20 | BIO_TYPE_FILTER,
    #
    BIO_TYPE_DGRAM = 21 | BIO_TYPE_SOURCE_SINK | BIO_TYPE_DESCRIPTOR,
    #
    BIO_TYPE_ASN1 = 22 | BIO_TYPE_FILTER,
    #
    BIO_TYPE_COMP = 23 | BIO_TYPE_FILTER,
    #
    BIO_TYPE_DGRAM_SCTP = 24 | BIO_TYPE_SOURCE_SINK | BIO_TYPE_DESCRIPTOR,
    #
    BIO_TYPE_CORE_TO_PROV = 25 | BIO_TYPE_SOURCE_SINK,
    #
    BIO_TYPE_START = 128)

# Some values are reserved until OpenSSL 3.0.0 because they were previously
# included in SSL_OP_ALL in a 1.1.x release.
@bitflag SSLOptions::Culong begin
    # Disable Extended master secret
    SSL_OP_NO_EXTENDED_MASTER_SECRET = 0x00000001
    # Cleanse plaintext copies of data delivered to the application
    SSL_OP_CLEANSE_PLAINTEXT = 0x00000002
    # Allow initial connection to servers that don't support RI
    SSL_OP_LEGACY_SERVER_CONNECT = 0x00000004
    SSL_OP_TLSEXT_PADDING = 0x00000010
    SSL_OP_SAFARI_ECDHE_ECDSA_BUG = 0x00000040
    SSL_OP_IGNORE_UNEXPECTED_EOF = 0x00000080
    SSL_OP_DISABLE_TLSEXT_CA_NAMES = 0x00000200
    # In TLSv1.3 allow a non-(ec)dhe based kex_mode
    SSL_OP_ALLOW_NO_DHE_KEX = 0x00000400
    # Disable SSL 3.0/TLS 1.0 CBC vulnerability workaround that was added in
    # OpenSSL 0.9.6d.  Usually (depending on the application protocol) the
    # workaround is not needed.  Unfortunately some broken SSL/TLS
    # implementations cannot handle it at all, which is why we include it in
    # SSL_OP_ALL. Added in 0.9.6e
    SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS = 0x00000800
    # DTLS options
    SSL_OP_NO_QUERY_MTU = 0x00001000
    # Turn on Cookie Exchange (on relevant for servers)
    SSL_OP_COOKIE_EXCHANGE = 0x00002000
    # Don't use RFC4507 ticket extension
    SSL_OP_NO_TICKET = 0x00004000
    # Use Cisco's "speshul" version of DTLS_BAD_VER
    # (only with deprecated DTLSv1_client_method())
    SSL_OP_CISCO_ANYCONNECT = 0x00008000
    # As server, disallow session resumption on renegotiation
    SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION = 0x00010000
    # Don't use compression even if supported
    SSL_OP_NO_COMPRESSION = 0x00020000
    # Permit unsafe legacy renegotiation
    SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION = 0x00040000
    # Disable encrypt-then-mac
    SSL_OP_NO_ENCRYPT_THEN_MAC = 0x00080000
    # Enable TLSv1.3 Compatibility mode. This is on by default.
    # A future version of OpenSSL may have this disabled by default.
    SSL_OP_ENABLE_MIDDLEBOX_COMPAT = 0x00100000
    # Prioritize Chacha20Poly1305 when client does.
    # Modifies SSL_OP_CIPHER_SERVER_PREFERENCE
    SSL_OP_PRIORITIZE_CHACHA = 0x00200000
    # Set on servers to choose the cipher according to the server's preferences
    SSL_OP_CIPHER_SERVER_PREFERENCE = 0x00400000
    # If set, a server will allow a client to issue a SSLv3.0 version number as
    # latest version supported in the premaster secret, even when TLSv1.0
    # (version 3.1) was announced in the client hello. Normally this is
    # forbidden to prevent version rollback attacks.
    SSL_OP_TLS_ROLLBACK_BUG = 0x00800000
    # Switches off automatic TLSv1.3 anti-replay protection for early data. This
    # is a server-side option only (no effect on the client).
    SSL_OP_NO_ANTI_REPLAY = 0x01000000
    SSL_OP_NO_SSLv3 = 0x02000000
    SSL_OP_NO_TLSv1 = 0x04000000
    SSL_OP_NO_TLSv1_2 = 0x08000000
    SSL_OP_NO_TLSv1_1 = 0x10000000
    SSL_OP_NO_TLSv1_3 = 0x20000000
    SSL_OP_NO_RENEGOTIATION = 0x40000000
    # Make server add server-hello extension from early version of cryptopro
    # draft, when GOST ciphersuite is negotiated. Required for interoperability
    # with CryptoPro CSP 3.x
    SSL_OP_CRYPTOPRO_TLSEXT_BUG = 0x80000000
end

const SSL_OP_NO_DTLSv1 = SSL_OP_NO_TLSv1
const SSL_OP_NO_DTLSv1_2 = SSL_OP_NO_TLSv1_2
const SSL_OP_NO_SSL_MASK = SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2 | SSL_OP_NO_TLSv1_3
const SSL_OP_NO_DTLS_MASK = SSL_OP_NO_DTLSv1 | SSL_OP_NO_DTLSv1_2

"""
    # SSL_OP_ALL: various bug workarounds that should be rather harmless.
    # This used to be 0x000FFFFFL before 0.9.7.
    # This used to be 0x80000BFFU before 1.1.1.
"""
const SSL_OP_ALL =
    SSL_OP_CRYPTOPRO_TLSEXT_BUG |
    SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS |
    SSL_OP_LEGACY_SERVER_CONNECT |
    SSL_OP_TLSEXT_PADDING |
    SSL_OP_SAFARI_ECDHE_ECDSA_BUG

"""
    OpenSSL init settings.
"""
@bitflag OpenSSLInitSettings::UInt32 begin
    OPENSSL_INIT_NO_LOAD_CRYPTO_STRINGS = 0x00000001
    OPENSSL_INIT_LOAD_CRYPTO_STRINGS = 0x00000002
    OPENSSL_INIT_ADD_ALL_CIPHERS = 0x00000004
    OPENSSL_INIT_ADD_ALL_DIGESTS = 0x00000008
    OPENSSL_INIT_NO_ADD_ALL_CIPHERS = 0x00000010
    OPENSSL_INIT_NO_ADD_ALL_DIGESTS = 0x00000020
    OPENSSL_INIT_LOAD_CONFIG = 0x00000040
    OPENSSL_INIT_NO_LOAD_CONFIG = 0x00000080
    OPENSSL_INIT_ASYNC = 0x00000100
    OPENSSL_INIT_ENGINE_RDRAND = 0x00000200
    OPENSSL_INIT_ENGINE_DYNAMIC = 0x00000400
    OPENSSL_INIT_ENGINE_OPENSSL = 0x00000800
    OPENSSL_INIT_ENGINE_CRYPTODEV = 0x00001000
    OPENSSL_INIT_ENGINE_CAPI = 0x00002000
    OPENSSL_INIT_ENGINE_PADLOCK = 0x00004000
    OPENSSL_INIT_ENGINE_AFALG = 0x00008000
    OPENSSL_INIT_ATFORK = 0x00020000
    OPENSSL_INIT_NO_ATEXIT = 0x00080000
    OPENSSL_INIT_NO_LOAD_SSL_STRINGS = 0x00100000
    OPENSSL_INIT_LOAD_SSL_STRINGS = 0x00200000
end

const OPENSSL_INIT_SSL_DEFAULT = OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS

"""
    TlsVersion.
"""
@enum(TlsVersion::Clong,
    TLS1_VERSION = 0x0301,
    TLS1_1_VERSION = 0x0302,
    TLS1_2_VERSION = 0x0303,
    TLS1_3_VERSION = 0x0304)

#
const MBSTRING_FLAG = 0x1000

@enum(MBStringFlags::Int32,
    # Utf8 encoding
    MBSTRING_UTF8 = MBSTRING_FLAG,
    # Latin1 encoding
    MBSTRING_ASC = MBSTRING_FLAG | 1,
    # UCS2 encoding
    MBSTRING_BMP = MBSTRING_FLAG | 2,
    # Universal string, Utf32 encoding
    MBSTRING_UNIV = MBSTRING_FLAG | 4)

# Longest known is SHA512.
const EVP_MAX_MD_SIZE = 64
const EVP_MAX_KEY_LENGTH = 64
const EVP_MAX_IV_LENGTH = 16
const EVP_MAX_BLOCK_LENGTH = 32

# define RSA_3   0x3L
# define RSA_F4  0x10001L

@enum(EvpPKeyType::Int32,
    # define NID_undef 0
    EVP_PKEY_NONE = 0,
    # define NID_rsaEncryption 6
    EVP_PKEY_RSA = 6,
    # define NID_rsa 19
    EVP_PKEY_RSA2 = 19,
    # define NID_rsassaPss 912
    EVP_PKEY_RSA_PSS = 912,
    # define NID_dsa 116
    EVP_PKEY_DSA = 116,
    # define NID_dsa_2 67
    EVP_PKEY_DSA1 = 67,
    # define NID_dsaWithSHA 66
    EVP_PKEY_DSA2 = 66,
    # define NID_dsaWithSHA1 113
    EVP_PKEY_DSA3 = 113,
    # define NID_dsaWithSHA1_2 70
    EVP_PKEY_DSA4 = 70,
    # define NID_dhKeyAgreement 28
    EVP_PKEY_DH = 28,
    # define NID_dhpublicnumber 920
    EVP_PKEY_DHX = 920,
    # define NID_X9_62_id_ecPublicKey 408
    EVP_PKEY_EC = 408,
    # define NID_sm2 1172
    EVP_PKEY_SM2 = 1172,
    # define NID_hmac 855
    EVP_PKEY_HMAC = 855,
    # define NID_cmac 894
    EVP_PKEY_CMAC = 894,
    # define NID_id_scrypt 973
    EVP_PKEY_SCRYPT = 973,
    # define NID_tls1_prf 1021
    EVP_PKEY_TLS1_PRF = 1021,
    # define NID_hkdf 1036
    EVP_PKEY_HKDF = 1036,
    # define NID_poly1305 1061
    EVP_PKEY_POLY1305 = 1061,
    # define NID_siphash 1062
    EVP_PKEY_SIPHASH = 1062,
    # define NID_X25519 1034
    EVP_PKEY_X25519 = 1034,
    # define NID_ED25519 1087
    EVP_PKEY_ED25519 = 1087,
    # define NID_X448 1035
    EVP_PKEY_X448 = 1035,
    # define NID_ED448 1088
    EVP_PKEY_ED448 = 1088)

const RSA_F4 = 0x10001

@enum(OpenSSLVersion::Int32,
    # The text variant of the version number and the release date.
    OPENSSL_VERSION = 0,
    # The compiler flags set for the compilation process.
    OPENSSL_CFLAGS = 1,
    # The date of the build process.
    OPENSSL_BUILT_ON = 2,
    # The "Configure" target of the library build.
    OPENSSL_PLATFORM = 3,
    # The "OPENSSLDIR" setting of the library build.
    OPENSSL_DIR = 4,
    # The "ENGINESDIR" setting of the library build.
    OPENSSL_ENGINES_DIR = 5,
    # The short version identifier string.
    OPENSSL_VERSION_STRING = 6,
    # The longer version identifier string
    OPENSSL_FULL_VERSION_STRING = 7,
    # The MODULESDIR setting of the library.
    OPENSSL_MODULES_DIR = 8,
    # The current OpenSSL cpu settings
    OPENSSL_CPU_INFO = 9)

@enum(SSLControlCommand::Cint,
    SSL_CTRL_SET_MIN_PROTO_VERSION = 123,
    SSL_CTRL_SET_MAX_PROTO_VERSION = 124,
    SSL_CTRL_SET_SPLIT_SEND_FRAGMENT = 125,
    SSL_CTRL_SET_MAX_PIPELINES = 126,
    SSL_CTRL_GET_TLSEXT_STATUS_REQ_TYPE = 127,
    SSL_CTRL_GET_TLSEXT_STATUS_REQ_CB = 128,
    SSL_CTRL_GET_TLSEXT_STATUS_REQ_CB_ARG = 129,
    SSL_CTRL_GET_MIN_PROTO_VERSION = 130,
    SSL_CTRL_GET_MAX_PROTO_VERSION = 131,
    SSL_CTRL_GET_SIGNATURE_NID = 132,
    SSL_CTRL_GET_TMP_KEY = 133,
    SSL_CTRL_GET_NEGOTIATED_GROUP = 134)

@enum(SSLErrorCode::Cint,
    SSL_ERROR_NONE = 0,
    SSL_ERROR_SSL = 1,
    SSL_ERROR_WANT_READ =  2,
    SSL_ERROR_WANT_WRITE = 3,
    SSL_ERROR_WANT_X509_LOOKUP = 4,
    SSL_ERROR_SYSCALL = 5,
    SSL_ERROR_ZERO_RETURN = 6,
    SSL_ERROR_WANT_CONNECT = 7,
    SSL_ERROR_WANT_ACCEPT = 8,
    SSL_ERROR_WANT_ASYNC = 9,
    SSL_ERROR_WANT_ASYNC_JOB = 10,
    SSL_ERROR_WANT_CLIENT_HELLO_CB = 11,
    SSL_ERROR_WANT_ACCEPT_RESUME = 12)

"""
    OpenSSL error.
"""
struct OpenSSLError <: Exception
    msg::AbstractString

    OpenSSLError() = new(get_error())
    OpenSSLError(msg::String) = new(msg)
    OpenSSLError(x::SSLErrorCode) = new(string(x))
end

"""
    version(; [version_type::OpenSSLVersion])

Obtain the version as a `String`. See [`OpenSSLVersion`](@ref) to select
the version type.

See also [`version_number`](@ref) to obtain a `VersionNumber`.
"""
function version(; version_type::OpenSSLVersion=OPENSSL_VERSION)::String
    version = ccall(
        (:OpenSSL_version, libcrypto),
        Cstring,
        (Cint,),
        version_type)

    return unsafe_string(version)
end

"""
    version_number()::VersionNumber

Return the version number of the OpenSSL C library.
This uses `OpenSSL_version_num()`.
"""
function version_number()
    # This works on OpenSSL v1.1
    vn = ccall((:OpenSSL_version_num, libcrypto), Culong, ())

    # 0xMNN00PP0L
    # M: major
    # NN: minor
    # PP: patch
    major = vn >> 28
    minor = vn >> 20 & 0xff
    patch = vn >> 4 & 0xff

    return VersionNumber(major, minor, patch)
end

@static if version_number() ≥ v"3"
    # In OpenSSL v3, macros redirect these symbols
    const SSL_get_peer_certificate = :SSL_get1_peer_certificate
    const EVP_PKEY_base_id = :EVP_PKEY_get_base_id
    const EVP_CIPHER_key_length = :EVP_CIPHER_get_key_length
    const EVP_CIPHER_iv_length = :EVP_CIPHER_get_iv_length
    const EVP_CIPHER_block_size = :EVP_CIPHER_get_block_size
    const EVP_CIPHER_CTX_block_size = :EVP_CIPHER_CTX_get_block_size
    const EVP_CIPHER_CTX_key_length = :EVP_CIPHER_CTX_get_key_length
    const EVP_CIPHER_CTX_iv_length = :EVP_CIPHER_CTX_get_iv_length
else
    const SSL_get_peer_certificate = :SSL_get_peer_certificate
    const EVP_PKEY_base_id = :EVP_PKEY_base_id
    const EVP_CIPHER_key_length = :EVP_CIPHER_key_length
    const EVP_CIPHER_iv_length = :EVP_CIPHER_iv_length
    const EVP_CIPHER_block_size = :EVP_CIPHER_block_size
    const EVP_CIPHER_CTX_block_size = :EVP_CIPHER_CTX_block_size
    const EVP_CIPHER_CTX_key_length = :EVP_CIPHER_CTX_key_length
    const EVP_CIPHER_CTX_iv_length = :EVP_CIPHER_CTX_iv_length
end

# Locate oss-modules, which contains legacy shared library
function _ossl_modules_path()
    @static if Sys.iswindows()
        bin_dir = dirname(OpenSSL_jll.libssl_path)
        lib_dir = joinpath(dirname(bin_dir), "lib")
        if Sys.WORD_SIZE == 64
            return joinpath(lib_dir * "64", "ossl-modules")
        else
            return joinpath(lib_dir, "ossl-modules")
        end
    else
        return joinpath(dirname(OpenSSL_jll.libssl_path), "ossl-modules")
    end
end

"""
    ossl_provider_set_default_search_path([libctx], [path])

Set the default search path for providers. If no arguments are given,
the global context will be configured for the ossl-modules directory
in the OpenSSL_jll artifact.

This is called with no arguments in OpenSSL.jl `__init__` when
OpenSSL v3 is used.

!!! compat "OpenSSL v3" `ossl_provider_set_default_search_path` is only available with version 3 of the OpenSSL_jll
"""
function ossl_provider_set_default_search_path(libctx = C_NULL, path = _ossl_modules_path())
    result = ccall(
        (:OSSL_PROVIDER_set_default_search_path, libcrypto),
        Cint,
        (Ptr{Nothing}, Cstring),
        libctx,
        path
    )
    if result == 0
        throw(OpenSSLError())
    end
    return result
end

"""
    load_provider([libctx], provider_name)

Load a provider. If libctx is omitted, the provider will be loaded into the
global context.

!!! compat "OpenSSL v3" `load_provider` is only available with version 3 of the OpenSSL_jll
"""
function load_provider(libctx, provider_name)
    result = ccall(
        (:OSSL_PROVIDER_load, libcrypto),
        Ptr{Nothing},
        (Ptr{Nothing}, Cstring),
        libctx,
        provider_name
    )
    if result == C_NULL
        throw(OpenSSLError())
    end
    return nothing
end
load_provider(provider_name) = load_provider(C_NULL, provider_name)

"""
    load_legacy_provider()

Load the legacy provider. This loads legacy ciphers such as Blowfish.

See https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html

!!! compat "OpenSSL v3" `load_legacy_provider` is only available with version 3 of the OpenSSL_jll
"""
load_legacy_provider() = load_provider(C_NULL, "legacy")

"""
    Random bytes.
"""
function random_bytes!(rand_data::Vector{UInt8})
    GC.@preserve rand_data begin
        if ccall(
            (:RAND_bytes, libcrypto),
            Cint,
            (Ptr{UInt8}, Cint),
            pointer(rand_data),
            length(rand_data)) != 1
            throw(OpenSSLError())
        end
    end
end

function random_bytes(length)
    rand_data = Vector{UInt8}(undef, length)
    random_bytes!(rand_data)

    return rand_data
end

"""
    Big number context.
"""
mutable struct BigNumContext
    bn_ctx::Ptr{Cvoid}

    function BigNumContext()
        big_num_contex = ccall(
            (:BN_CTX_secure_new, libcrypto),
            Ptr{Cvoid},
            ())
        if big_num_contex == C_NULL
            throw(OpenSSLError())
        end

        big_num_contex = new(big_num_contex)
        finalizer(free, big_num_contex)

        return big_num_contex
    end
end

function free(big_num_contex::BigNumContext)
    ccall(
        (:BN_CTX_free, libcrypto),
        Cvoid,
        (BigNumContext,),
        big_num_contex)

    big_num_contex.bn_ctx = C_NULL
    return nothing
end

"""
    Big number, multiprecision integer arithmetics.
"""
mutable struct BigNum
    bn::Ptr{Cvoid}

    function BigNum(bn::Ptr{Cvoid})
        big_num = new(bn)
        finalizer(free, big_num)

        return big_num
    end

    function BigNum()
        bn = ccall(
            (:BN_new, libcrypto),
            Ptr{Cvoid},
            ())
        if bn == C_NULL
            throw(OpenSSLError())
        end

        big_num = BigNum(bn)

        return big_num
    end

    BigNum(value::UInt8) = BigNum(UInt64(value))

    BigNum(value::UInt32) = BigNum(UInt64(value))

    function BigNum(value::UInt64)
        big_num = BigNum()
        if ccall(
            (:BN_set_word, libcrypto),
            Cint,
            (BigNum, UInt64),
            big_num, value) != 1
            throw(OpenSSLError())
        end

        return big_num
    end
end

function free(big_num::BigNum)
    ccall(
        (:BN_free, libcrypto),
        Cvoid,
        (BigNum,),
        big_num)

    big_num.bn = C_NULL
    return nothing
end

function Base.:(==)(big_num_1::BigNum, big_num_2::BigNum)
    result = ccall(
        (:BN_cmp, libcrypto),
        Cint,
        (BigNum, BigNum),
        big_num_1,
        big_num_2)

    if result == -2
        throw(OpenSSLError())
    end

    return result == 0
end

"""
    BigNum does not support up ref. Duplicate the big number instead.
"""
function up_ref(big_num::BigNum)::Ptr{Cvoid}
    big_num = ccall(
        (:BN_dup, libcrypto),
        Ptr{Cvoid},
        (BigNum,),
        big_num)
    if big_num == C_NULL
        throw(OpenSSLError())
    end

    return big_num
end

function Base.:+(a::BigNum, b::BigNum)::BigNum
    r = BigNum()

    if ccall(
        (:BN_add, libcrypto),
        Cint,
        (BigNum, BigNum, BigNum),
        r,
        a,
        b) != 1
        throw(OpenSSLError())
    end

    return r
end

function Base.:-(a::BigNum, b::BigNum)::BigNum
    r = BigNum()

    if ccall(
        (:BN_sub, libcrypto),
        Cint,
        (BigNum, BigNum, BigNum),
        r,
        a,
        b) != 1
        throw(OpenSSLError())
    end

    return r
end

function Base.:*(a::BigNum, b::BigNum)::BigNum
    r = BigNum()

    c = BigNumContext()

    if ccall(
        (:BN_mul, libcrypto),
        Cint,
        (BigNum, BigNum, BigNum, BigNumContext),
        r,
        a,
        b,
        c) != 1
        throw(OpenSSLError())
    end

    finalize(c)

    return r
end

function Base.:/(a::BigNum, b::BigNum)::BigNum
    dv = BigNum()
    rm = BigNum()

    c = BigNumContext()

    if ccall(
        (:BN_div, libcrypto),
        Cint,
        (BigNum, BigNum, BigNum, BigNum, BigNumContext),
        dv,
        rm,
        a,
        b,
        c) != 1
        throw(OpenSSLError())
    end

    finalize(rm)
    finalize(c)

    return dv
end

function Base.:%(a::BigNum, b::BigNum)::BigNum
    dv = BigNum()
    rm = BigNum()

    c = BigNumContext()

    if ccall(
        (:BN_div, libcrypto),
        Cint,
        (BigNum, BigNum, BigNum, BigNum, BigNumContext),
        dv,
        rm,
        a,
        b,
        c) != 1
        throw(OpenSSLError())
    end

    finalize(dv)
    finalize(c)

    return rm
end

"""
    EVP_CIPHER.
"""
mutable struct EvpCipher
    evp_cipher::Ptr{Cvoid}
end

get_block_size(evp_cipher::EvpCipher)::Int32 = ccall(
    (EVP_CIPHER_block_size, libcrypto),
    Int32,
    (EvpCipher,),
    evp_cipher)

get_key_length(evp_cipher::EvpCipher)::Int32 = ccall(
    (EVP_CIPHER_key_length, libcrypto),
    Int32,
    (EvpCipher,),
    evp_cipher)

get_init_vector_length(evp_cipher::EvpCipher)::Int32 = ccall(
    (EVP_CIPHER_iv_length, libcrypto),
    Int32,
    (EvpCipher,),
    evp_cipher)

function Base.getproperty(evp_cipher::EvpCipher, name::Symbol)
    if name === :block_size
        return get_block_size(evp_cipher)
    elseif name === :key_length
        return get_key_length(evp_cipher)
    elseif name === :init_vector_length
        return get_init_vector_length(evp_cipher)
    else
        # fallback to getfield
        return getfield(evp_cipher, name)
    end
end

"""
    Basic Block Cipher Modes:
    - ECB Electronic Code Block
    - CBC Cipher Block Chaining
    - CFB Cipher Feedback
    - OFB Output Feedback
"""

"""
    Blowfish encryption algorithm in CBC, ECB, CFB and OFB modes.
"""
EvpBlowFishCBC()::EvpCipher = EvpCipher(ccall((:EVP_bf_cbc, libcrypto), Ptr{Cvoid}, ()))

EvpBlowFishECB()::EvpCipher = EvpCipher(ccall((:EVP_bf_ecb, libcrypto), Ptr{Cvoid}, ()))

EvpBlowFishCFB()::EvpCipher = EvpCipher(ccall((:EVP_bf_cfb, libcrypto), Ptr{Cvoid}, ()))

EvpBlowFishOFB()::EvpCipher = EvpCipher(ccall((:EVP_bf_ofb, libcrypto), Ptr{Cvoid}, ()))

"""
    AES with a 128-bit key in CBC, ECB, CFB and OFB modes.
"""
EvpAES128CBC()::EvpCipher = EvpCipher(ccall((:EVP_aes_128_cbc, libcrypto), Ptr{Cvoid}, ()))

EvpAES128ECB()::EvpCipher = EvpCipher(ccall((:EVP_aes_128_ecb, libcrypto), Ptr{Cvoid}, ()))

EvpAES128CFB()::EvpCipher = EvpCipher(ccall((:EVP_aes_128_cfb, libcrypto), Ptr{Cvoid}, ()))

EvpAES128OFB()::EvpCipher = EvpCipher(ccall((:EVP_aes_128_ofb, libcrypto), Ptr{Cvoid}, ()))

"""
    Null cipher, does nothing.
"""
EvpEncNull()::EvpCipher = EvpCipher(ccall((:EVP_enc_null, libcrypto), Ptr{Cvoid}, ()))

mutable struct EvpCipherContext
    evp_cipher_ctx::Ptr{Cvoid}

    function EvpCipherContext(evp_cipher_ctx::Ptr{Cvoid})
        evp_cipher_ctx = new(evp_cipher_ctx)
        finalizer(free, evp_cipher_ctx)

        return evp_cipher_ctx
    end

    function EvpCipherContext()
        evp_cipher_ctx = ccall(
            (:EVP_CIPHER_CTX_new, libcrypto),
            Ptr{Cvoid},
            ())
        if evp_cipher_ctx == C_NULL
            throw(OpenSSLError())
        end

        return EvpCipherContext(evp_cipher_ctx)
    end
end

function free(evp_cipher_ctx::EvpCipherContext)
    ccall(
        (:EVP_CIPHER_CTX_free, libcrypto),
        Cvoid,
        (EvpCipherContext,),
        evp_cipher_ctx)

    evp_cipher_ctx.evp_cipher_ctx = C_NULL
    return nothing
end

function decrypt_init(
    evp_cipher_ctx::EvpCipherContext,
    evp_cipher::EvpCipher,
    symetric_key::Vector{UInt8},
    init_vector::Vector{UInt8})
    # Initialize decryption context.

    GC.@preserve symetric_key init_vector begin
        if length(symetric_key) == get_key_length(evp_cipher)
            # Use default key length.
            if ccall(
                (:EVP_DecryptInit_ex, libcrypto),
                Cint,
                (EvpCipherContext, EvpCipher, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
                evp_cipher_ctx,
                evp_cipher,
                C_NULL,
                pointer(symetric_key),
                pointer(init_vector)) != 1
                throw(OpenSSLError())
            end
        else
            if ccall(
                (:EVP_DecryptInit_ex, libcrypto),
                Cint,
                (EvpCipherContext, EvpCipher, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
                evp_cipher_ctx,
                evp_cipher,
                C_NULL,
                pointer(symetric_key),
                pointer(init_vector)) != 1
                throw(OpenSSLError())
            end

            if ccall(
                (:EVP_CIPHER_CTX_set_key_length, libcrypto),
                Cint,
                (EvpCipherContext, Cint),
                evp_cipher_ctx,
                length(symetric_key)) != 1
                throw(OpenSSLError())
            end

            if ccall(
                (:EVP_DecryptInit_ex, libcrypto),
                Cint,
                (EvpCipherContext, EvpCipher, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
                evp_cipher_ctx,
                evp_cipher,
                C_NULL,
                pointer(symetric_key),
                pointer(init_vector)) != 1
                throw(OpenSSLError())
            end
        end
    end
end

function encrypt_init(
    evp_cipher_ctx::EvpCipherContext,
    evp_cipher::EvpCipher,
    symetric_key::Vector{UInt8},
    init_vector::Vector{UInt8})
    # Initialize encryption context.

    GC.@preserve symetric_key init_vector begin
        if length(symetric_key) == get_key_length(evp_cipher)
            # Use default key length.
            if ccall(
                (:EVP_EncryptInit_ex, libcrypto),
                Cint,
                (EvpCipherContext, EvpCipher, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
                evp_cipher_ctx,
                evp_cipher,
                C_NULL,
                pointer(symetric_key),
                pointer(init_vector)) != 1
                throw(OpenSSLError())
            end
        else
            if ccall(
                (:EVP_EncryptInit_ex, libcrypto),
                Cint,
                (EvpCipherContext, EvpCipher, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
                evp_cipher_ctx,
                evp_cipher,
                C_NULL,
                pointer(symetric_key),
                pointer(init_vector)) != 1
                throw(OpenSSLError())
            end

            if ccall(
                (:EVP_CIPHER_CTX_set_key_length, libcrypto),
                Cint,
                (EvpCipherContext, Cint),
                evp_cipher_ctx,
                length(symetric_key)) != 1
                throw(OpenSSLError())
            end

            if ccall(
                (:EVP_EncryptInit_ex, libcrypto),
                Cint,
                (EvpCipherContext, EvpCipher, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
                evp_cipher_ctx,
                evp_cipher,
                C_NULL,
                pointer(symetric_key),
                pointer(init_vector)) != 1
                throw(OpenSSLError())
            end
        end
    end
end

function cipher_update(evp_cipher_ctx::EvpCipherContext, in_data::Vector{UInt8})::Vector{UInt8}
    in_length = length(in_data)

    block_size = get_block_size(evp_cipher_ctx)
    enc_length = Int((in_length + block_size - 1) / block_size * block_size)

    out_data = Vector{UInt8}(undef, enc_length)
    out_length = Ref{UInt32}(0)

    GC.@preserve in_data out_data out_length begin
        if ccall(
            (:EVP_CipherUpdate, libcrypto),
            Cint,
            (EvpCipherContext, Ptr{UInt8}, Ptr{Int32}, Ptr{UInt8}, Cint),
            evp_cipher_ctx,
            pointer(out_data),
            pointer_from_objref(out_length),
            pointer(in_data),
            in_length) != 1
            throw(OpenSSLError())
        end
    end

    resize!(out_data, out_length.x)

    return out_data
end

function cipher_final(evp_cipher_ctx::EvpCipherContext)::Vector{UInt8}
    block_size = get_block_size(evp_cipher_ctx)

    out_data = Vector{UInt8}(undef, block_size)
    out_length = Ref{Int32}(0)

    GC.@preserve out_data out_length begin
        if ccall(
            (:EVP_CipherFinal_ex, libcrypto),
            Cint,
            (EvpCipherContext, Ptr{UInt8}, Ptr{Int32}),
            evp_cipher_ctx,
            pointer(out_data),
            pointer_from_objref(out_length)) != 1
            throw(OpenSSLError())
        end
    end

    resize!(out_data, out_length.x)

    return out_data
end

function cipher(evp_cipher_ctx::EvpCipherContext, in_io::IO, out_io::IO)
    while !eof(in_io)
        available_bytes = bytesavailable(in_io)
        in_data = read(in_io, available_bytes)
        write(out_io, cipher_update(evp_cipher_ctx, in_data))
    end

    write(out_io, cipher_final(evp_cipher_ctx))

    return nothing
end

get_block_size(evp_cipher_ctx::EvpCipherContext)::Int32 = ccall(
    (EVP_CIPHER_CTX_block_size, libcrypto),
    Int32,
    (EvpCipherContext,),
    evp_cipher_ctx)

get_key_length(evp_cipher_ctx::EvpCipherContext)::Int32 = ccall(
    (EVP_CIPHER_CTX_key_length, libcrypto),
    Int32,
    (EvpCipherContext,),
    evp_cipher_ctx)

get_init_vector_length(evp_cipher_ctx::EvpCipherContext)::Int32 = ccall(
    (EVP_CIPHER_CTX_iv_length, libcrypto),
    Int32,
    (EvpCipherContext,),
    evp_cipher_ctx)

function Base.getproperty(evp_cipher_ctx::EvpCipherContext, name::Symbol)
    if name === :block_size
        return get_block_size(evp_cipher_ctx)
    elseif name === :key_length
        return get_key_length(evp_cipher_ctx)
    elseif name === :init_vector_length
        return get_init_vector_length(evp_cipher_ctx)
    else
        # fallback to getfield
        return getfield(evp_cipher_ctx, name)
    end
end

"""
    EVP Message Digest.
"""
mutable struct EvpDigest
    evp_md::Ptr{Cvoid}
end

EvpMDNull()::EvpDigest = EvpDigest(ccall((:EVP_md_null, libcrypto), Ptr{Cvoid}, ()))

EvpMD2()::EvpDigest = EvpDigest(ccall((:EVP_md2, libcrypto), Ptr{Cvoid}, ()))

EvpMD5()::EvpDigest = EvpDigest(ccall((:EVP_md5, libcrypto), Ptr{Cvoid}, ()))

EvpSHA1()::EvpDigest = EvpDigest(ccall((:EVP_sha1, libcrypto), Ptr{Cvoid}, ()))

EvpSHA224()::EvpDigest = EvpDigest(ccall((:EVP_sha224, libcrypto), Ptr{Cvoid}, ()))

EvpSHA256()::EvpDigest = EvpDigest(ccall((:EVP_sha256, libcrypto), Ptr{Cvoid}, ()))

EvpSHA384()::EvpDigest = EvpDigest(ccall((:EVP_sha384, libcrypto), Ptr{Cvoid}, ()))

EvpSHA512()::EvpDigest = EvpDigest(ccall((:EVP_sha512, libcrypto), Ptr{Cvoid}, ()))

EvpDSS1()::EvpDigest = EvpDigest(ccall((:EVP_dss1, libcrypto), Ptr{Cvoid}, ()))

"""
    EVP Message Digest Context.
"""
mutable struct EvpDigestContext
    evp_md_ctx::Ptr{Cvoid}

    function EvpDigestContext()
        evp_digest_ctx = ccall(
            (:EVP_MD_CTX_new, libcrypto),
            Ptr{Cvoid},
            ())
        if evp_digest_ctx == C_NULL
            throw(OpenSSLError())
        end

        evp_digest_ctx = new(evp_digest_ctx)
        finalizer(free, evp_digest_ctx)

        return evp_digest_ctx
    end
end

function free(evp_digest_ctx::EvpDigestContext)
    ccall(
        (:EVP_MD_CTX_free, libcrypto),
        Cvoid,
        (EvpDigestContext,),
        evp_digest_ctx)

    evp_digest_ctx.evp_md_ctx = C_NULL
    return nothing
end

function digest_init(evp_digest_ctx::EvpDigestContext, evp_digest::EvpDigest)
    if ccall(
        (:EVP_DigestInit_ex, libcrypto),
        Cint,
        (EvpDigestContext, EvpDigest, Ptr{Cvoid}),
        evp_digest_ctx,
        evp_digest,
        C_NULL) != 1
        throw(OpenSSLError())
    end
end

function digest_update(evp_digest_ctx::EvpDigestContext, in_data::Vector{UInt8})
    GC.@preserve in_data begin
        if ccall(
            (:EVP_DigestUpdate, libcrypto),
            Cint,
            (EvpDigestContext, Ptr{UInt8}, Csize_t),
            evp_digest_ctx,
            pointer(in_data),
            length(in_data)) != 1
            throw(OpenSSLError())
        end
    end
end

function digest_final(evp_digest_ctx::EvpDigestContext)::Vector{UInt8}
    out_data = Vector{UInt8}(undef, EVP_MAX_MD_SIZE)
    out_length = Ref{UInt32}(0)

    GC.@preserve out_data out_length begin
        if ccall(
            (:EVP_DigestFinal_ex, libcrypto),
            Cint,
            (EvpDigestContext, Ptr{UInt8}, Ptr{UInt32}),
            evp_digest_ctx,
            pointer(out_data),
            pointer_from_objref(out_length)) != 1
            throw(OpenSSLError())
        end
    end

    resize!(out_data, out_length.x)

    return out_data
end

"""
    Computes the message digest (hash).
"""
function digest(evp_digest::EvpDigest, io::IO)
    md_ctx = EvpDigestContext()

    digest_init(md_ctx, evp_digest)

    while !eof(io)
        available_bytes = bytesavailable(io)
        in_data = read(io, available_bytes)
        digest_update(md_ctx, in_data)
    end

    result = digest_final(md_ctx)

    finalize(md_ctx)

    return result
end

"""
    RSA structure.
    The RSA structure consists of several BIGNUM components.
    It can contain public as well as private RSA keys.
"""
mutable struct RSA
    rsa::Ptr{Cvoid}

    function RSA()
        rsa = ccall(
            (:RSA_new, libcrypto),
            Ptr{Cvoid},
            ())
        if rsa == C_NULL
            throw(OpenSSLError())
        end

        rsa = new(rsa)
        finalizer(free, rsa)

        return rsa
    end
end

function free(rsa::RSA)
    ccall(
        (:RSA_free, libcrypto),
        Cvoid,
        (RSA,),
        rsa)

    rsa.rsa = C_NULL
    return nothing
end

"""
    Generate RSA key pair.
"""
function rsa_generate_key(; bits::Int32=Int32(2048))::RSA
    rsa = RSA()
    big_num = BigNum(UInt64(RSA_F4))

    if ccall(
        (:RSA_generate_key_ex, libcrypto),
        Cint,
        (RSA, Cint, BigNum, Ptr{Cvoid}),
        rsa,
        bits,
        big_num,
        C_NULL) != 1
        throw(OpenSSLError())
    end

    return rsa
end

"""
    DSA structure.
"""
mutable struct DSA
    dsa::Ptr{Cvoid}

    function DSA()
        dsa = ccall(
            (:DSA_new, libcrypto),
            Ptr{Cvoid},
            ())
        if dsa == C_NULL
            throw(OpenSSLError())
        end

        dsa = new(dsa)
        finalizer(free, dsa)

        return dsa
    end
end

function free(dsa::DSA)
    ccall(
        (:DSA_free, libcrypto),
        Cvoid,
        (DSA,),
        dsa)

    dsa.dsa = C_NULL
    return nothing
end

"""
    Generate DSA key pair.
"""
function dsa_generate_key(; bits::Int32=Int32(1024))::DSA
    dsa = DSA()

    if ccall(
        (:DSA_generate_parameters_ex, libcrypto),
        Cint,
        (DSA, Cint, Ptr{UInt8}, Cint, Ptr{Cint}, Ptr{Culong}, Ptr{Cvoid}),
        dsa,
        bits,
        C_NULL,
        1,
        C_NULL,
        C_NULL,
        C_NULL) != 1
        throw(OpenSSLError())
    end

    return dsa
end

"""
    OpenSSL BIOMethod.
"""
mutable struct BIOMethod
    bio_method::Ptr{Cvoid}

    BIOMethod(bio_method::Ptr{Cvoid}) = new(bio_method)

    function BIOMethod(bio_type::String)
        bio_method_index = ccall(
            (:BIO_get_new_index, libcrypto),
            Cint,
            ())
        if bio_method_index == -1
            throw(OpenSSLError())
        end

        bio_method = ccall(
            (:BIO_meth_new, libcrypto),
            Ptr{Cvoid},
            (Cint, Cstring),
            bio_method_index,
            bio_type)
        if bio_method == C_NULL
            throw(OpenSSLError())
        end

        bio_method = new(bio_method)
        finalizer(free, bio_method)

        if ccall(
            (:BIO_meth_set_create, libcrypto),
            Cint,
            (BIOMethod, Ptr{Cvoid}),
            bio_method,
            BIO_STREAM_CALLBACKS.x.on_bio_create_ptr) != 1
            throw(OpenSSLError())
        end

        if ccall(
            (:BIO_meth_set_destroy, libcrypto),
            Cint,
            (BIOMethod, Ptr{Cvoid}),
            bio_method,
            BIO_STREAM_CALLBACKS.x.on_bio_destroy_ptr) != 1
            throw(OpenSSLError())
        end

        if ccall(
            (:BIO_meth_set_read, libcrypto),
            Cint,
            (BIOMethod, Ptr{Cvoid}),
            bio_method,
            BIO_STREAM_CALLBACKS.x.on_bio_read_ptr) != 1
            throw(OpenSSLError())
        end

        if ccall(
            (:BIO_meth_set_write, libcrypto),
            Cint,
            (BIOMethod, Ptr{Cvoid}),
            bio_method,
            BIO_STREAM_CALLBACKS.x.on_bio_write_ptr) != 1
            throw(OpenSSLError())
        end

        if ccall(
            (:BIO_meth_set_puts, libcrypto),
            Cint,
            (BIOMethod, Ptr{Cvoid}),
            bio_method,
            BIO_STREAM_CALLBACKS.x.on_bio_puts_ptr) != 1
            throw(OpenSSLError())
        end

        if ccall(
            (:BIO_meth_set_ctrl, libcrypto),
            Cint,
            (BIOMethod, Ptr{Cvoid}),
            bio_method,
            BIO_STREAM_CALLBACKS.x.on_bio_ctrl_ptr) != 1
            throw(OpenSSLError())
        end

        return bio_method
    end
end

"""
    Creates a file descriptor BIO method.
"""
function BIOMethod_fd()::BIOMethod
    bio_method = ccall(
        (:BIO_s_fd, libcrypto),
        Ptr{Cvoid},
        ())

    return BIOMethod(bio_method)
end

"""
    Creates a memory BIO method.
"""
function BIOMethodMemory()::BIOMethod
    bio_method = ccall(
        (:BIO_s_mem, libcrypto),
        Ptr{Cvoid},
        ())

    return BIOMethod(bio_method)
end

function BIOMethodSecureMemory()::BIOMethod
    bio_method = ccall(
        (:BIO_s_secmem, libcrypto),
        Ptr{Cvoid},
        ())

    return BIOMethod(bio_method)
end

function free(bio_method::BIOMethod)
    ccall(
        (:BIO_meth_free, libcrypto),
        Cvoid,
        (BIOMethod,),
        bio_method)

    bio_method.bio_method = C_NULL
    return nothing
end

"""
    BIO.
"""
mutable struct BIO
    bio::Ptr{Cvoid}

    BIO(bio::Ptr{Cvoid}) = new(bio)

    """
        Creates a BIO object using IO stream method.
        The BIO object is not registered with the finalizer.
    """
    function BIO(data=nothing; finalize::Bool=true)
        bio = ccall(
            (:BIO_new, libcrypto),
            Ptr{Cvoid},
            (BIOMethod,),
            BIO_STREAM_METHOD.x)
        if bio == C_NULL
            throw(OpenSSLError())
        end

        bio = new(bio)
        finalize && finalizer(free, bio)

        # note that `data` must be held as a reference somewhere else
        # since it is not referenced by the BIO directly
        # e.g. in SSLStream, we keep the `io` reference that is passed to
        # the read/write BIOs
        ccall(
            (:BIO_set_data, libcrypto),
            Cvoid,
            (BIO, Ptr{Cvoid}),
            bio,
            data === nothing ? C_NULL : pointer_from_objref(data))

        # Set BIO as non-blocking
        ccall(
            (:BIO_ctrl, libcrypto),
            Cint,
            (BIO, Cint, Cint, Ptr{Cvoid}),
            bio,
            102,
            1,
            C_NULL)
        # Mark BIO as initalized.
        ccall(
            (:BIO_set_init, libcrypto),
            Cvoid,
            (BIO, Cint),
            bio,
            1)

        ccall(
            (:BIO_set_shutdown, libcrypto),
            Cvoid,
            (BIO, Cint),
            bio,
            0)

        return bio
    end

    """
        Creates BIO for given BIOMethod.
    """
    function BIO(bio_method::BIOMethod)
        bio = ccall(
            (:BIO_new, libcrypto),
            Ptr{Cvoid},
            (BIOMethod,),
            bio_method)
        if bio == C_NULL
            throw(OpenSSLError())
        end

        bio = new(bio)
        finalizer(free, bio)

        return bio
    end
end

function free(bio::BIO)
    ccall(
        (:BIO_free, libcrypto),
        Cvoid,
        (BIO,),
        bio)

    bio.bio = C_NULL
    return nothing
end

clear(bio::BIO) = bio.bio

"""
    Returns the BIO type.
"""
function bio_type(bio::BIO)::BIOType
    bio_type = ccall(
        (:BIO_method_type, libcrypto),
        Cint,
        (BIO,),
        bio)

    return BIOType(bio_type)
end

"""
    Returns internal BIO memory.
"""
function bio_get_mem_data(bio::BIO)
    if bio_type(bio) != BIO_TYPE_MEM
        throw(ArgumentError("Expecting BIO_TYPE_MEM bio."))
    end

    # Get BIOs memory data.
    # define BIO_get_mem_data(b,pp)  BIO_ctrl(b,BIO_CTRL_INFO,0,(char *)(pp))
    mem_ptr = Ref{Ptr{UInt8}}(0)
    mem_data = GC.@preserve mem_ptr begin
        result = ccall(
            (:BIO_ctrl, libcrypto),
            Clong,
            (BIO, BIOCtrl, Clong, Ptr{Ptr{Cvoid}}),
            bio,
            BIO_CTRL_INFO,
            0,
            pointer_from_objref(mem_ptr))
        if mem_ptr.x != C_NULL
            return unsafe_wrap(Vector{UInt8}, mem_ptr.x, result; own=false)
        else
            return Vector{UInt8}()
        end
    end

    return mem_data
end

"""
    BIO write.
"""
## throw error here
function Base.unsafe_write(bio::BIO, out_buffer::Ptr{UInt8}, out_length::Int)
    result = ccall(
        (:BIO_write, libcrypto),
        Cint,
        (BIO, Ptr{Cvoid}, Cint),
        bio,
        out_buffer,
        out_length)
    if result < 0
        throw(OpenSSLError())
    end
end

Base.write(bio::BIO, out_data) = return unsafe_write(bio, pointer(out_data), length(out_data))

"""
    ASN1_TIME.
"""
mutable struct Asn1Time
    asn1_time::Ptr{Cvoid}

    Asn1Time(asn1_time::Ptr{Cvoid}) = new(asn1_time)

    Asn1Time() = Asn1Time(0)

    Asn1Time(date_time::DateTime) = Asn1Time(Int64(floor(datetime2unix(date_time))))

    function Asn1Time(unix_time::Int)
        asn1_time = ccall(
            (:ASN1_TIME_set, libcrypto),
            Ptr{Cvoid},
            (Ptr{Cvoid}, Int64),
            C_NULL,
            unix_time)
        if asn1_time == C_NULL
            throw(OpenSSLError())
        end

        asn1_time = new(asn1_time)
        finalizer(free, asn1_time)

        return asn1_time
    end
end

function free(asn1_time::Asn1Time)
    ccall(
        (:ASN1_STRING_free, libcrypto),
        Cvoid,
        (Asn1Time,),
        asn1_time)

    asn1_time.asn1_time = C_NULL
    return nothing
end

function Dates.adjust(asn1_time::Asn1Time, seconds::Second)
    adj_asn1_time = ccall(
        (:X509_gmtime_adj, libcrypto),
        Ptr{Cvoid},
        (Asn1Time, Int64),
        asn1_time,
        seconds.value)
    if adj_asn1_time == C_NULL
        throw(OpenSSLError())
    end

    if (adj_asn1_time != asn1_time.asn1_time)
        free(asn1_time)
        asn1_time.asn1_time = adj_asn1_time
    end
end

Dates.adjust(asn1_time::Asn1Time, days::Day) = Dates.adjust(asn1_time, Second(days))

Dates.adjust(asn1_time::Asn1Time, years::Year) = Dates.adjust(asn1_time, Day(365 * years.value))

"""
    EVP_PKEY, EVP Public Key interface.
"""
mutable struct EvpPKey
    evp_pkey::Ptr{Cvoid}

    function EvpPKey(evp_pkey::Ptr{Cvoid})::EvpPKey
        evp_pkey = new(evp_pkey)
        finalizer(free, evp_pkey)

        return evp_pkey
    end

    function EvpPKey()::EvpPKey
        evp_pkey = ccall(
            (:EVP_PKEY_new, libcrypto),
            Ptr{Cvoid},
            ())
        if evp_pkey == C_NULL
            throw(OpenSSLError())
        end

        return EvpPKey(evp_pkey)
    end

    function EvpPKey(rsa::RSA)::EvpPKey
        evp_pkey = EvpPKey()

        if ccall(
            (:EVP_PKEY_assign, libcrypto),
            Cint, (EvpPKey, Cint, RSA),
            evp_pkey,
            EVP_PKEY_RSA,
            rsa) != 1
            throw(OpenSSLError())
        end

        rsa.rsa = C_NULL
        return evp_pkey
    end

    function EvpPKey(dsa::DSA)::EvpPKey
        evp_pkey = EvpPKey()

        if ccall(
            (:EVP_PKEY_assign, libcrypto),
            Cint, (EvpPKey, Cint, DSA),
            evp_pkey,
            EVP_PKEY_DSA,
            dsa) != 1
            throw(OpenSSLError())
        end

        dsa.dsa = C_NULL
        return evp_pkey
    end

    """
        Creates a EvpPKey from PEM string.
    """
    function EvpPKey(in_string::AbstractString)::EvpPKey
        # Create a BIO and write the PEM string.
        bio = BIO(BIOMethodMemory())
        unsafe_write(bio, pointer(in_string), length(in_string))

        evp_pkey = ccall(
            (:PEM_read_bio_PrivateKey, libcrypto),
            Ptr{Cvoid},
            (BIO, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
            bio,
            C_NULL,
            C_NULL,
            C_NULL)
        if evp_pkey == C_NULL
            throw(OpenSSLError())
        end

        free(bio)

        return EvpPKey(evp_pkey)
    end
end

function free(evp_pkey::EvpPKey)
    ccall(
        (:EVP_PKEY_free, libcrypto),
        Cvoid,
        (EvpPKey,),
        evp_pkey)

    evp_pkey.evp_pkey = C_NULL
    return nothing
end

function Base.:(==)(evp_pkey_1::EvpPKey, evp_pkey_2::EvpPKey)
    result = ccall(
        (:EVP_PKEY_cmp, libcrypto),
        Cint,
        (EvpPKey, EvpPKey),
        evp_pkey_1,
        evp_pkey_2)

    if result == -2
        throw(OpenSSLError())
    end

    return result == 1
end

function Base.write(io::IO, evp_pkey::EvpPKey, evp_cipher::EvpCipher=EvpCipher(C_NULL))
    bio = BIO(io)

    GC.@preserve io begin
        if ccall(
            (:PEM_write_bio_PrivateKey, libcrypto),
            Cint,
            (BIO, EvpPKey, EvpCipher, Ptr{Cvoid}, Cint, Cint, Ptr{Cvoid}),
            bio,
            evp_pkey,
            evp_cipher,
            C_NULL,
            0,
            0,
            C_NULL) != 1
            throw(OpenSSLError())
        end
    end
end

function get_key_type(evp_pkey::EvpPKey)::EvpPKeyType
    pkey_type = ccall(
        (EVP_PKEY_base_id, libcrypto),
        EvpPKeyType,
        (EvpPKey,),
        evp_pkey)

    return pkey_type
end

function Base.getproperty(evp_pkey::EvpPKey, name::Symbol)
    if name === :key_type
        return get_key_type(evp_pkey)
    else
        # fallback to getfield
        return getfield(evp_pkey, name)
    end
end

"""
    Stack Of.
"""
mutable struct StackOf{T}
    sk::Ptr{Cvoid}

    function StackOf{T}(sk::Ptr{Cvoid}) where {T}
        stack_of = new(sk)

        finalizer(free, stack_of)
        return stack_of
    end

    function StackOf{T}() where {T}
        sk = ccall(
            (:OPENSSL_sk_new_null, libcrypto),
            Ptr{Cvoid},
            ())
        if sk == C_NULL
            throw(OpenSSLError())
        end

        return StackOf{T}(sk)
    end
end

function free(stack_of::StackOf{T}) where {T}
    ccall(
        (:OPENSSL_sk_free, libcrypto),
        Cvoid,
        (StackOf{T},),
        stack_of)

    stack_of.sk = C_NULL
    return nothing
end

function Base.push!(stack_of::StackOf{T}, element::T) where {T}
    count = ccall(
        (:OPENSSL_sk_push, libcrypto),
        Cint,
        (StackOf{T}, Ptr{Cvoid}),
        stack_of,
        up_ref(element))

    if count == 0
        throw(OpenSSLError())
    end

    return count
end

function Base.pop!(stack_of::StackOf{T}) where {T}
    ptr = ccall(
        (:OPENSSL_sk_pop, libcrypto),
        Ptr{Cvoid},
        (StackOf{T},),
        stack_of)

    return T(ptr)
end

function Base.length(stack_of::StackOf{T}) where {T}
    return ccall(
        (:OPENSSL_sk_num, libcrypto),
        Cint,
        (StackOf{T},),
        stack_of)
end

"""
    X509 Name.
"""
mutable struct X509Name
    x509_name::Ptr{Cvoid}

    function X509Name(x509_name::Ptr{Cvoid})
        x509_name = new(x509_name)
        finalizer(free, x509_name)

        return x509_name
    end

    function X509Name()
        x509_name = ccall(
            (:X509_NAME_new, libcrypto),
            Ptr{Cvoid},
            ())
        if x509_name == C_NULL
            throw(OpenSSLError())
        end

        x509_name = X509Name(x509_name)

        return x509_name
    end
end

function free(x509_name::X509Name)
    ccall(
        (:X509_NAME_free, libcrypto),
        Cvoid,
        (X509Name,),
        x509_name)

    x509_name.x509_name = C_NULL
    return nothing
end

function Base.:(==)(x509_name_1::X509Name, x509_name_2::X509Name)
    result = ccall(
        (:X509_NAME_cmp, libcrypto),
        Cint,
        (X509Name, X509Name),
        x509_name_1,
        x509_name_2)

    if result == -2
        throw(OpenSSLError())
    end

    return result == 0
end

"""
    X509Name to string.
"""
function Base.String(x509_name::X509Name)::String
    name_ptr = ccall(
        (:X509_NAME_oneline, libcrypto),
        Cstring,
        (X509Name, Ptr{UInt8}, Cint),
        x509_name,
        C_NULL,
        0)
    if name_ptr == C_NULL
        throw(OpenSSLError())
    end

    str = unsafe_string(name_ptr)

    ccall(
        (:CRYPTO_free, libcrypto),
        Cvoid,
        (Cstring,),
        name_ptr)

    return str
end

function add_entry(x509_name::X509Name, field::String, value::String)
    if ccall(
        (:X509_NAME_add_entry_by_txt, libcrypto),
        Cint,
        (X509Name, Cstring, Cint, Cstring, Cint, Cint, Cint),
        x509_name,
        field,
        MBSTRING_ASC,
        value,
        -1,
        -1,
        0) != 1
        throw(OpenSSLError())
    end

    return nothing
end

"""
    X509 Attribute.
"""
mutable struct X509Attribute
    x509_attr::Ptr{Cvoid}

    function X509Attribute(x509_attr::Ptr{Cvoid})
        x509_attr = new(x509_attr)
        finalizer(free, x509_attr)

        return x509_attr
    end

    function X509Attribute()
        x509_attr = ccall(
            (:X509_ATTRIBUTE_new, libcrypto),
            Ptr{Cvoid},
            ())
        if x509_attr == C_NULL
            throw(OpenSSLError())
        end

        x509_attr = X509Attribute(x509_attr)

        return x509_attr
    end
end

function free(x509_attr::X509Attribute)
    ccall(
        (:X509_ATTRIBUTE_free, libcrypto),
        Cvoid,
        (X509Attribute,),
        x509_attr)

    x509_attr.x509_attr = C_NULL
    return nothing
end

"""
    X509Attribute does not support up ref. Duplicate the attribute instead.
"""
function up_ref(x509_attr::X509Attribute)::Ptr{Cvoid}
    x509_attr = ccall(
        (:X509_ATTRIBUTE_dup, libcrypto),
        Ptr{Cvoid},
        (X509Attribute,),
        x509_attr)
    if x509_attr == C_NULL
        throw(OpenSSLError())
    end

    return x509_attr
end

"""
    X509_EXTENSION
"""
mutable struct X509Extension
    x509_ext::Ptr{Cvoid}

    function X509Extension(x509_ext::Ptr{Cvoid})
        x509_ext = new(x509_ext)

        finalizer(free, x509_ext)
        return x509_ext
    end

    function X509Extension(name::String, value::String)
        x509_ext = ccall(
            (:X509V3_EXT_conf, libcrypto),
            Ptr{Cvoid},
            (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Cstring),
            C_NULL,
            C_NULL,
            name,
            value)
        if x509_ext == C_NULL
            throw(OpenSSLError())
        end

        return X509Extension(x509_ext)
    end
end

function free(x509_ext::X509Extension)
    ccall(
        (:X509_EXTENSION_free, libcrypto),
        Cvoid,
        (X509Extension,),
        x509_ext)

    x509_ext.x509_ext = C_NULL
    return nothing
end

"""
    X509Extension does not support up ref. Duplicate the extension instead.
"""
function up_ref(x509_ext::X509Extension)::Ptr{Cvoid}
    x509_ext = ccall(
        (:X509_EXTENSION_dup, libcrypto),
        Ptr{Cvoid},
        (X509Extension,),
        x509_ext)
    if x509_ext == C_NULL
        throw(OpenSSLError())
    end

    return x509_ext
end

"""
    X509 Certificate.
"""
mutable struct X509Certificate
    x509::Ptr{Cvoid}

    function X509Certificate()
        x509 = ccall(
            (:X509_new, libcrypto),
            Ptr{Cvoid},
            ())
        if x509 == C_NULL
            throw(OpenSSLError())
        end

        return X509Certificate(x509)
    end

    function X509Certificate(x509::Ptr{Cvoid})
        x509_cert = new(x509)
        finalizer(free, x509_cert)

        return x509_cert
    end

    """
        Creates a X509 certificate from PEM string.
    """
    function X509Certificate(in_string::AbstractString)::X509Certificate
        # Create a BIO and write the PEM string.
        bio = BIO(BIOMethodMemory())
        unsafe_write(bio, pointer(in_string), length(in_string))

        x509 = ccall(
            (:PEM_read_bio_X509, libcrypto),
            Ptr{Cvoid},
            (BIO, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}),
            bio,
            C_NULL,
            C_NULL,
            C_NULL)
        if x509 == C_NULL
            throw(OpenSSLError())
        end

        free(bio)

        return X509Certificate(x509)
    end
end

function free(x509_cert::X509Certificate)
    ccall(
        (:X509_free, libcrypto),
        Cvoid,
        (X509Certificate,),
        x509_cert)

    x509_cert.x509 = C_NULL
    return nothing
end

function up_ref(x509_cert::X509Certificate)::Ptr{Cvoid}
    _ = ccall(
        (:X509_up_ref, libcrypto),
        Cint,
        (X509Certificate,),
        x509_cert)

    return x509_cert.x509
end

function Base.:(==)(x509_cert_1::X509Certificate, x509_cert_2::X509Certificate)
    result = ccall(
        (:X509_cmp, libcrypto),
        Cint,
        (X509Certificate, X509Certificate),
        x509_cert_1,
        x509_cert_2)

    if result == -2
        throw(OpenSSLError())
    end

    return result == 0
end

function Base.write(io::IO, x509_cert::X509Certificate)
    bio = BIO(io)

    GC.@preserve io begin
        if ccall(
            (:PEM_write_bio_X509, libcrypto),
            Cint,
            (BIO, X509Certificate),
            bio,
            x509_cert) != 1
            throw(OpenSSLError())
        end
    end
end

function sign_certificate(x509_cert::X509Certificate, evp_pkey::EvpPKey)
    evp_md = ccall(
        (:EVP_sha256, libcrypto),
        Ptr{Cvoid},
        ())

    if ccall(
        (:X509_sign, libcrypto),
        Cint,
        (X509Certificate, EvpPKey, Ptr{Cvoid}),
        x509_cert,
        evp_pkey,
        evp_md) == 0
        throw(OpenSSLError())
    end

    if ccall(
        (:X509_verify, libcrypto),
        Cint,
        (X509Certificate, EvpPKey),
        x509_cert,
        evp_pkey) != 1
        throw(OpenSSLError())
    end
end

function add_extension(x509_cert::X509Certificate, x509_ext::X509Extension)
    if ccall(
        (:X509_add_ext, libcrypto),
        Cint,
        (X509Certificate, X509Extension, Cint),
        x509_cert,
        x509_ext,
        -1) != 1
        throw(OpenSSLError())
    end
end

function get_subject_name(x509_cert::X509Certificate)::X509Name
    x509_name = ccall(
        (:X509_get_subject_name, libcrypto),
        Ptr{Cvoid},
        (X509Certificate,),
        x509_cert)

    # Duplicate x509_name as it is an internal pointer and must not be freed.
    x509_name = ccall(
        (:X509_NAME_dup, libcrypto),
        Ptr{Cvoid},
        (Ptr{Cvoid},),
        x509_name)

    if x509_name == C_NULL
        throw(OpenSSLError())
    end

    return X509Name(x509_name)
end

function set_subject_name(x509_cert::X509Certificate, x509_name::X509Name)
    if ccall(
        (:X509_set_subject_name, libcrypto),
        Cint,
        (X509Certificate, X509Name),
        x509_cert,
        x509_name) != 1
        throw(OpenSSLError())
    end
end

function get_issuer_name(x509_cert::X509Certificate)::X509Name
    x509_name = ccall(
        (:X509_get_issuer_name, libcrypto),
        Ptr{Cvoid},
        (X509Certificate,),
        x509_cert)

    # Duplicate x509_name as it is an internal pointer and must not be freed.
    x509_name = ccall(
        (:X509_NAME_dup, libcrypto),
        Ptr{Cvoid},
        (Ptr{Cvoid},),
        x509_name)

    if x509_name == C_NULL
        throw(OpenSSLError())
    end

    return X509Name(x509_name)
end

function set_issuer_name(x509_cert::X509Certificate, x509_name::X509Name)
    if ccall(
        (:X509_set_issuer_name, libcrypto),
        Cint, (X509Certificate, X509Name),
        x509_cert,
        x509_name) != 1
        throw(OpenSSLError())
    end
end

function get_time_not_before(x509_cert::X509Certificate)::Asn1Time
    asn1_time = ccall(
        (:X509_getm_notBefore, libcrypto),
        Ptr{Cvoid},
        (X509Certificate,),
        x509_cert)
    if asn1_time == C_NULL
        throw(OpenSSLError())
    end

    asn1_time = Asn1Time(asn1_time)
    return asn1_time
end

function set_time_not_before(x509_cert::X509Certificate, asn1_time::Asn1Time)
    if ccall(
        (:X509_set1_notBefore, libcrypto),
        Cint,
        (X509Certificate, Asn1Time),
        x509_cert,
        asn1_time) != 1
        throw(OpenSSLError())
    end
end

function get_time_not_after(x509_cert::X509Certificate)::Asn1Time
    asn1_time = ccall(
        (:X509_getm_notAfter, libcrypto),
        Ptr{Cvoid},
        (X509Certificate,),
        x509_cert)
    if asn1_time == C_NULL
        throw(OpenSSLError())
    end

    asn1_time = Asn1Time(asn1_time)
    return asn1_time
end

function set_time_not_after(x509_cert::X509Certificate, asn1_time::Asn1Time)
    if ccall(
        (:X509_set1_notAfter, libcrypto),
        Cint,
        (X509Certificate, Asn1Time),
        x509_cert,
        asn1_time) != 1
        throw(OpenSSLError())
    end
end

function get_version(x509_cert::X509Certificate)::Int
    version = ccall(
        (:X509_get_version, libcrypto),
        Clong,
        (X509Certificate,),
        x509_cert)

    return Int(version)
end

function set_version(x509_cert::X509Certificate, version::Int)
    if ccall(
        (:X509_set_version, libcrypto),
        Cint,
        (X509Certificate, Cint),
        x509_cert,
        version) != 1
        throw(OpenSSLError())
    end
end

function get_public_key(x509_cert::X509Certificate)::EvpPKey
    evp_pkey = ccall(
        (:X509_get_pubkey, libcrypto),
        Ptr{Cvoid},
        (X509Certificate,),
        x509_cert)
    if evp_pkey == C_NULL
        throw(OpenSSLError())
    end

    return EvpPKey(evp_pkey)
end

function set_public_key(x509_cert::X509Certificate, evp_pkey::EvpPKey)
    if ccall(
        (:X509_set_pubkey, libcrypto),
        Cint,
        (X509Certificate, EvpPKey),
        x509_cert,
        evp_pkey) != 1
        throw(OpenSSLError())
    end
end

function Base.getproperty(x509_cert::X509Certificate, name::Symbol)
    if name === :subject_name
        return get_subject_name(x509_cert)
    elseif name === :issuer_name
        return get_issuer_name(x509_cert)
    elseif name === :time_not_before
        return get_time_not_before(x509_cert)
    elseif name === :time_not_after
        return get_time_not_after(x509_cert)
    elseif name === :version
        return get_version(x509_cert)
    elseif name === :public_key
        return get_public_key(x509_cert)
    else
        # fallback to getfield
        return getfield(x509_cert, name)
    end
end

function Base.setproperty!(x509_cert::X509Certificate, name::Symbol, value)
    if name === :subject_name
        set_subject_name(x509_cert, value)
    elseif name === :issuer_name
        set_issuer_name(x509_cert, value)
    elseif name === :time_not_before
        set_time_not_before(x509_cert, value)
    elseif name === :time_not_after
        set_time_not_after(x509_cert, value)
    elseif name === :version
        set_version(x509_cert, value)
    elseif name === :public_key
        set_public_key(x509_cert, value)
    else
        # fallback to setfield
        setfield!(x509_cert, name, value)
    end
end

"""
    X509 Request.
"""
mutable struct X509Request
    x509_req::Ptr{Cvoid}

    function X509Request()
        x509_req = ccall(
            (:X509_REQ_new, libcrypto),
            Ptr{Cvoid},
            ())
        if x509_req == C_NULL
            throw(OpenSSLError())
        end

        return X509Request(x509_req)
    end

    function X509Request(x509::Ptr{Cvoid})
        x509_req = new(x509)
        finalizer(free, x509_req)

        return x509_req
    end
end

function free(x509_req::X509Request)
    ccall(
        (:X509_REQ_free, libcrypto),
        Cvoid,
        (X509Request,),
        x509_req)

    x509_req.x509_req = C_NULL
    return nothing
end

function Base.write(io::IO, x509_req::X509Request)
    bio = BIO(io)

    GC.@preserve io begin
        if ccall(
            (:PEM_write_bio_X509_REQ, libcrypto),
            Cint,
            (BIO, X509Request),
            bio,
            x509_req) != 1
            throw(OpenSSLError())
        end
    end
end

function add_extensions(x509_req::X509Request, x509_exts::StackOf{X509Extension})
    if ccall(
        (:X509_REQ_add_extensions, libcrypto),
        Cint,
        (X509Request, StackOf{X509Extension}),
        x509_req,
        x509_exts) != 1
        throw(OpenSSLError())
    end
end

function sign_request(x509_req::X509Request, evp_pkey::EvpPKey)
    evp_md = ccall(
        (:EVP_sha256, libcrypto),
        Ptr{Cvoid},
        ())

    if ccall(
        (:X509_REQ_set_pubkey, libcrypto),
        Cint,
        (X509Request, EvpPKey),
        x509_req,
        evp_pkey) != 1
        throw(OpenSSLError())
    end

    if ccall(
        (:X509_REQ_sign, libcrypto),
        Cint, (X509Request, EvpPKey, Ptr{Cvoid}),
        x509_req,
        evp_pkey,
        evp_md) == 0
        throw(OpenSSLError())
    end

    if ccall(
        (:X509_REQ_verify, libcrypto),
        Cint, (X509Request, EvpPKey),
        x509_req,
        evp_pkey) != 1
        throw(OpenSSLError())
    end
end

function get_subject_name(x509_req::X509Request)::X509Name
    x509_name = ccall(
        (:X509_REQ_get_subject_name, libcrypto),
        Ptr{Cvoid},
        (X509Request,),
        x509_req)

    # Duplicate x509_name as it is an internal pointer and must not be freed.
    x509_name = ccall(
        (:X509_NAME_dup, libcrypto),
        Ptr{Cvoid},
        (Ptr{Cvoid},),
        x509_name)

    if x509_name == C_NULL
        throw(OpenSSLError())
    end

    return X509Name(x509_name)
end

function set_subject_name(x509_req::X509Request, x509_name::X509Name)
    if ccall(
        (:X509_REQ_set_subject_name, libcrypto),
        Cint,
        (X509Request, X509Name),
        x509_req,
        x509_name) != 1
        throw(OpenSSLError())
    end
end

function get_public_key(x509_req::X509Request)::EvpPKey
    evp_pkey = ccall(
        (:X509_REQ_get_pubkey, libcrypto),
        Ptr{Cvoid},
        (X509Request,),
        x509_req)
    if evp_pkey == C_NULL
        throw(OpenSSLError())
    end

    return EvpPKey(evp_pkey)
end

function set_public_key(x509_req::X509Request, evp_pkey::EvpPKey)
    if ccall(
        (:X509_REQ_set_pubkey, libcrypto),
        Cint,
        (X509Request, EvpPKey),
        x509_req,
        evp_pkey) != 1
        throw(OpenSSLError())
    end
end

function set_version(x509_req::X509Request, version::Int)
    if ccall(
        (:X509_REQ_set_version, libcrypto),
        Cint,
        (X509Request, Cint),
        x509_req,
        version) != 1
        throw(OpenSSLError())
    end
end

function get_version(x509_req::X509Request)::Int
    version = ccall((:X509_REQ_get_version, libcrypto), Clong, (X509Request,), x509_req)
    return Int(version)
end

function get_extensions(x509_req::X509Request)
    sk = ccall(
        (:X509_REQ_get_extensions, libcrypto),
        Ptr{Cvoid},
        (X509Request,),
        x509_req)
    if sk == C_NULL
        throw(OpenSSLError())
    end

    return StackOf{X509Extension}(sk)
end

function Base.getproperty(x509_req::X509Request, name::Symbol)
    if name === :subject_name
        return get_subject_name(x509_req)
    elseif name === :public_key
        return get_public_key(x509_req)
    elseif name === :extensions
        return get_extensions(x509_req)
    elseif name === :version
        return get_version(x509_req)
    else
        # fallback to getfield
        return getfield(x509_req, name)
    end
end

function Base.setproperty!(x509_req::X509Request, name::Symbol, value)
    if name === :subject_name
        set_subject_name(x509_req, value)
    elseif name === :public_key
        set_public_key(x509_req, value)
    elseif name === :version
        set_version(x509_req, value)
    else
        # fallback to setfield
        setfield!(x509_req, name, value)
    end
end

"""
    X509 Store.
"""
mutable struct X509Store
    x509_store::Ptr{Cvoid}

    function X509Store(x509_store::Ptr{Cvoid})
        x509_store = new(x509_store)
        finalizer(free, x509_store)

        return x509_store
    end

    function X509Store()
        x509_store = ccall(
            (:X509_STORE_new, libcrypto),
            Ptr{Cvoid},
            ())
        if x509_store == C_NULL
            throw(OpenSSLError())
        end

        return X509Store(x509_store)
    end
end

function free(x509_store::X509Store)
    ccall(
        (:X509_STORE_free, libcrypto),
        Cvoid,
        (X509Store,),
        x509_store)

    x509_store.x509_store = C_NULL
    return nothing
end

function add_cert(x509_store::X509Store, x509_cert::X509Certificate)
    if ccall(
        (:X509_STORE_add_cert, libcrypto),
        Cint,
        (X509Store, X509Certificate),
        x509_store,
        x509_cert) != 1
        throw(OpenSSLError())
    end
end

"""
    PKCS12 - Public Key Cryptography Standard #12
"""
mutable struct P12Object
    pkcs12::Ptr{Cvoid}

    function P12Object()
        pkcs12 = ccall(
            (:PKCS12_new, libcrypto),
            Ptr{Cvoid},
            ())
        if pkcs12 == C_NULL
            throw(OpenSSLError())
        end

        return P12Object(pkcs12)
    end

    function P12Object(pkcs12::Ptr{Cvoid})
        p12_object = new(pkcs12)
        finalizer(free, p12_object)

        return p12_object
    end

    function P12Object(evp_pkey::EvpPKey, x509_cert::X509Certificate)
        pkcs12 = ccall(
            (:PKCS12_create, libcrypto),
            Ptr{Cvoid},
            (Cstring, Cstring, EvpPKey, X509Certificate, Ptr{Cvoid}, Cint, Cint, Cint, Cint, Cint),
            C_NULL,
            C_NULL,
            evp_pkey,
            x509_cert,
            C_NULL,
            0,
            0,
            0,
            0,
            0)
        if pkcs12 == C_NULL
            throw(OpenSSLError())
        end

        return P12Object(pkcs12)
    end
end

function free(p12_object::P12Object)
    ccall(
        (:PKCS12_free, libcrypto),
        Cvoid,
        (P12Object,),
        p12_object)

    p12_object.pkcs12 = C_NULL
    return nothing
end

function Base.write(io::IO, p12_object::P12Object)
    bio = BIO(io)

    GC.@preserve io begin
        if ccall(
            (:i2d_PKCS12_bio, libcrypto),
            Cint,
            (BIO, P12Object),
            bio,
            p12_object) != 1
            throw(OpenSSLError())
        end
    end
end

function unpack(p12_object::P12Object)
    evp_pkey::EvpPKey = EvpPKey(C_NULL)
    x509_cert::X509Certificate = X509Certificate(C_NULL)
    x509_ca_stack::StackOf{X509Certificate} = StackOf{X509Certificate}(C_NULL)

    if ccall(
        (:PKCS12_parse, libcrypto),
        Cint,
        (P12Object, Cstring, Ref{EvpPKey}, Ref{X509Certificate}, Ref{StackOf{X509Certificate}}),
        p12_object,
        C_NULL,
        evp_pkey,
        x509_cert,
        x509_ca_stack) != 1
        throw(OpenSSLError())
    end

    return evp_pkey, x509_cert, x509_ca_stack
end

"""
    Crypto Init.
    Initialize OpenSSL library.
"""
mutable struct OpenSSLInit
    function OpenSSLInit()

        opts = UInt64(
            OPENSSL_INIT_LOAD_CRYPTO_STRINGS |
            OPENSSL_INIT_ADD_ALL_CIPHERS |
            OPENSSL_INIT_ADD_ALL_DIGESTS |
            OPENSSL_INIT_ASYNC)

        if ccall(
            (:OPENSSL_init_crypto, libcrypto),
            Cint,
            (UInt64, Ptr{Cvoid}),
            opts,
            C_NULL) != 1
            throw(OpenSSLError())
        end

        if ccall(
            (:OPENSSL_init_ssl, libssl),
            Cint,
            (UInt64, Ptr{Cvoid}),
            OPENSSL_INIT_LOAD_SSL_STRINGS,
            C_NULL) != 1
            throw(OpenSSLError())
        end

        return new()
    end
end

function Base.String(big_num::BigNum)
    bio = BIO(BIOMethodMemory())

    write(bio, "0x")

    if ccall(
        (:BN_print, libcrypto),
        Cint,
        (BIO, BigNum),
        bio,
        big_num) != 1
        throw(OpenSSLError())
    end

    result = String(bio_get_mem_data(bio))

    free(bio)

    return result
end

function Base.String(asn1_time::Asn1Time)
    if asn1_time.asn1_time == C_NULL
        return "C_NULL"
    end

    bio = BIO(BIOMethodMemory())

    if ccall(
        (:ASN1_TIME_print, libcrypto),
        Cint,
        (BIO, Asn1Time),
        bio,
        asn1_time) != 1
        throw(OpenSSLError())
    end

    result = String(bio_get_mem_data(bio))

    free(bio)

    return result
end

function Base.String(x509_cert::X509Certificate)
    io = IOBuffer()

    println(io,
        """
subject_name: $(x509_cert.subject_name)
issuer_name: $(x509_cert.issuer_name)
time_not_before: $(x509_cert.time_not_before)
time_not_after: $(x509_cert.time_not_after)""")

    return String(take!(io))
end

function Base.String(x509_ext::X509Extension)
    if x509_ext.x509_ext == C_NULL
        return "C_NULL"
    end

    io = IOBuffer()

    bio = BIO(BIOMethodMemory())

    _ = ccall(
        (:X509V3_EXT_print, libcrypto),
        Cint,
        (BIO, X509Extension, Cint, Ptr{Cvoid}),
        bio,
        x509_ext,
        0,
        C_NULL)
    update_tls_error_state()

    result = String(bio_get_mem_data(bio))

    free(bio)

    return result
end

function Base.String(x509_cert::X509Request)
    io = IOBuffer()

    println(io, """subject_name: $(x509_cert.subject_name)""")

    return String(take!(io))
end

function Base.String(evp_pkey::EvpPKey)
    io = IOBuffer()

    bio = BIO(BIOMethodMemory())

    _ = ccall(
        (:EVP_PKEY_print_public, libcrypto),
        Cint,
        (BIO, EvpPKey, Cint, Ptr{Cvoid}),
        bio,
        evp_pkey,
        0,
        C_NULL)

    _ = ccall(
        (:EVP_PKEY_print_private, libcrypto),
        Cint,
        (BIO, EvpPKey, Cint, Ptr{Cvoid}),
        bio,
        evp_pkey,
        0,
        C_NULL)

    _ = ccall(
        (:EVP_PKEY_print_params, libcrypto),
        Cint,
        (BIO, EvpPKey, Cint, Ptr{Cvoid}),
        bio,
        evp_pkey,
        0,
        C_NULL)

    update_tls_error_state()

    result = String(bio_get_mem_data(bio))

    free(bio)

    return result
end

Base.show(io::IO, big_num::BigNum) = write(io, String(big_num))

Base.show(io::IO, asn1_time::Asn1Time) = write(io, String(asn1_time))

Base.show(io::IO, x509_name::X509Name) = write(io, String(x509_name))

Base.show(io::IO, x509_cert::X509Certificate) = write(io, String(x509_cert))

Base.show(io::IO, x509_ext::X509Extension) = write(io, String(x509_ext))

Base.show(io::IO, x509_req::X509Request) = write(io, String(x509_req))

Base.show(io::IO, evp_pkey::EvpPKey) = write(io, String(evp_pkey))

"""
    Error handling.
"""
function get_error()::String
    # Create memory BIO
    bio = BIO(BIOMethodMemory())

    local error_msg::String

    # Check existing error messages stored in task TLS.
    if haskey(task_local_storage(), :openssl_err)
        # Copy existing error from task TLS.
        tls_msg = task_local_storage(:openssl_err)
        delete!(task_local_storage(), :openssl_err)

        # Clear the error queue, print the error messages to the memory BIO.
        ccall(
            (:ERR_print_errors, libcrypto),
            Cvoid,
            (BIO,),
            bio)

        bio_msg = String(bio_get_mem_data(bio))
        error_msg = "$(tls_msg) : $(bio_msg)"
    else
        # Clear the error queue, print the error messages to the memory BIO.
        ccall(
            (:ERR_print_errors, libcrypto),
            Cvoid,
            (BIO,),
            bio)

        error_msg = String(bio_get_mem_data(bio))
    end

    # Read the formatted error messages from the memory BIO.

    # Ensure the queue is clear (if ERR_print_errors fails).
    clear_errors!()

    # Free bio.
    free(bio)

    return error_msg
end

function clear_errors!()
    ccall(
        (:ERR_clear_error, libcrypto),
        Cvoid,
        ())
end

function OpenSSLError(ret::Integer)
    ptr = ccall(
        (:ERR_error_string, libcrypto),
        Ptr{UInt8},
        (Culong, Ptr{UInt8}),
        Base.bitcast(Culong, Clong(ret)), C_NULL
    )
    return OpenSSLError(unsafe_string(ptr))
end

"""
    Copy and clear OpenSSL error queue to the task TLS .
"""
function update_tls_error_state()
    # Check if there are errors in OpenSSL error queue.
    if ccall(
        (:ERR_peek_error, libcrypto),
        Culong,
        ()) != 0
        # Clear OpenSSL queue and store the errors in the Task TLS.
        error_str = get_error()
        task_local_storage(:openssl_err, error_str)
    end
end

include("ssl.jl")

const OPEN_SSL_INIT = Ref{OpenSSLInit}()
const BIO_STREAM_CALLBACKS = Ref{BIOStreamCallbacks}()
const BIO_STREAM_METHOD = Ref{BIOMethod}()

"""
    Initialize module.
"""
function __init__()
    OPEN_SSL_INIT.x = OpenSSLInit()
    BIO_STREAM_CALLBACKS.x = BIOStreamCallbacks()
    BIO_STREAM_METHOD.x = BIOMethod("BIO_STREAM_METHOD")

    # Set the openssl provider search path
    if version_number() ≥ v"3"
        ossl_provider_set_default_search_path()
    end

    return
end

end # OpenSSL module
