using MbedTLS, Test, Random, Distributed

# by adding a single worker, we load the julia-shipped mbedtls binaries and thus ensure
# the rest of the MbedTLS.jl tests run propertly even in the presence of alternative
# versioned mbedtls libraries
addprocs(1)

import Sockets: connect

# Message digests
@test digest(MD_SHA256, "test", "secret") ==
   UInt8[0x03, 0x29, 0xa0, 0x6b, 0x62, 0xcd, 0x16, 0xb3, 0x3e, 0xb6, 0x79, 0x2b, 0xe8, 0xc6, 0x0b, 0x15, 0x8d, 0x89, 0xa2, 0xee, 0x3a, 0x87, 0x6f, 0xce, 0x9a, 0x88, 0x1e, 0xbb, 0x48, 0x8c, 0x09, 0x14]

# Symmetric encryption
let
    secret_key = rand(UInt8, 32)
    message = "Testing symmetric encryption"
    iv  = rand(UInt8, 16)

    cipher_text = encrypt(CIPHER_AES, secret_key, message, iv)
    plain_text = decrypt(CIPHER_AES, secret_key, cipher_text, iv)

    @test message == String(plain_text)
end

# openssl rsa -inform PEM -text -noout < key.pem
# keyPEMN =
# """
#     00:bc:42:e0:e8:31:52:fd:af:e3:32:42:c4:28:5b:
#     e9:cf:50:af:e9:20:c7:96:37:5c:cb:09:e6:90:5c:
#     ac:fc:8c:03:22:7c:68:70:32:f0:7b:e4:52:85:a2:
#     12:61:ac:82:16:a6:66:e8:e9:a2:f4:dc:b7:05:0f:
#     e0:21:ea:0d:6d:0d:3e:fe:5e:c2:6a:59:36:52:c9:
#     66:5a:54:b9:f7:80:43:41:48:56:b5:b3:50:bd:2a:
#     29:8c:81:96:29:64:0a:80:82:a4:dc:93:28:8f:43:
#     d5:0b:4b:3a:4f:c4:be:a0:ff:3f:c6:68:16:f0:c5:
#     be:04:16:fc:b6:52:c4:d8:f9:4d:66:bc:d6:b8:ce:
#     4f:a4:5a:97:66:fb:5d:db:1c:e9:e6:89:ab:f6:82:
#     9e:de:93:f7:3d:b4:35:77:5e:ae:1f:67:14:29:a4:
#     00:df:7c:2c:3d:76:42:d5:76:08:ff:11:09:20:fc:
#     bd:8a:8d:d4:a9:ce:2e:35:2b:c6:d2:de:dc:ad:1b:
#     8d:01:7c:c5:32:9a:8e:c4:f7:a6:94:55:d3:4b:96:
#     1b:ee:a0:94:95:5f:b2:a0:b0:f8:bb:02:b0:a5:a9:
#     0e:62:f1:a2:8a:3a:6f:ec:c2:e9:5e:b5:73:45:cb:
#     35:86:24:07:e8:11:28:25:7d:ce:35:c0:48:ff:a0:
#     f6:13
# """
keyPEMN = 23765780372307576820243785798710539106220019827391701276528023465129289386491638200309724085345164197157471148949804917921135961746756129955755123246417065664000080083079385610461361129162124204308601470870363093163972105641380345473969785909726960802402051498557030325897388318234260665809255594699414895859065471822450027547355233222204156733689900551990147367094140650142549959621935662238290254703866686898285439063843022440258882238926771918029490099100463478790421686785230680564068945032213969236010272566088532861337469543246408184082656216674316765444657485587779489134661155841677954252447171077733538133523
keyPEMe = BigInt(65537)

function verify_key_pem(data, signature)
    pubkey = RSA(MbedTLS.MBEDTLS_RSA_PKCS_V15, MD_SHA1)
    @test_throws MbedTLS.MbedException MbedTLS.pubkey_from_vals!(pubkey, keyPEMN, keyPEMe)
    MbedTLS.pubkey_from_vals!(pubkey, keyPEMe, keyPEMN)
    MbedTLS.verify(pubkey, MD_SHA1, MbedTLS.digest(MD_SHA1, data), signature)
end

# RSA
let
    key = MbedTLS.gen_key(MersenneTwister(0))
    # todo: test encryption/decryption

    # Test signature verification.
    # Pre-generated signature of "MbedTLS.jl" using key.pem
    signature =
    UInt8[0x35,0x6d,0xa0,0xdd,0x02,0x06,0xc2,0x8e,0x21,0xcf,0x34,0x48,0x59,0x39,
          0x61,0x2c,0x37,0x50,0x74,0x2f,0x1c,0x25,0x49,0x52,0x1c,0xaf,0xb0,0xb5,
          0x2d,0x52,0x4d,0xa0,0x45,0x86,0x23,0xc8,0xf4,0xc6,0xb1,0xa2,0x25,0x86,
          0x5b,0xe6,0xcc,0x34,0x21,0xd6,0x31,0x36,0x0c,0xaf,0x97,0xa4,0x7e,0xf4,
          0xe3,0x1a,0x5f,0x58,0x78,0x57,0xbf,0x1f,0xfd,0x5e,0xe0,0xc2,0x9a,0x33,
          0x66,0xb2,0x4b,0x41,0xb2,0x37,0xac,0x63,0xd2,0x7d,0x9b,0x76,0x2f,0xe8,
          0x66,0x1e,0x83,0x4f,0x45,0x47,0x76,0x3f,0xcc,0x91,0xbb,0x8b,0xc2,0x12,
          0x8a,0x69,0x83,0x39,0xca,0x87,0xbd,0x36,0xa0,0x5e,0x21,0x60,0x39,0x9b,
          0xf0,0x3c,0x84,0x19,0x92,0xfc,0x76,0xaa,0x71,0xd8,0x39,0x0c,0x5a,0x2c,
          0x82,0xe3,0x77,0xc1,0xcc,0x7b,0xb0,0xfb,0x3f,0xb4,0x4a,0xf1,0xaa,0xa8,
          0xae,0x1e,0x46,0xde,0x42,0xc4,0x1d,0x9b,0xe9,0x58,0xaf,0x36,0xb5,0xbf,
          0x35,0x92,0x94,0x5e,0x73,0xca,0x83,0x34,0x05,0xe3,0xf5,0x8c,0x18,0x3a,
          0xb6,0xea,0xe0,0xc9,0x54,0x83,0x13,0x1f,0xd0,0x4e,0x4e,0x26,0xdb,0xec,
          0x8a,0x98,0x99,0x03,0x09,0x49,0xd5,0x10,0x2f,0xc4,0x61,0xc7,0x9d,0x5f,
          0x37,0x10,0x49,0x28,0x72,0xb5,0x43,0xba,0x96,0x98,0x2c,0xb1,0x86,0x6e,
          0x04,0x80,0x61,0xdb,0x00,0x77,0x31,0x14,0xf3,0x31,0x0e,0xc6,0xda,0x40,
          0x73,0xe7,0x3e,0x1f,0xa6,0x45,0x29,0x43,0xcb,0xe2,0xff,0x1e,0xe8,0xaa,
          0x4f,0x0b,0x01,0xcb,0x4f,0xf2,0xe4,0xc9,0xfd,0x1d,0xb8,0x5c,0x40,0xf2,
          0xeb,0x76,0x2b,0x5f]
    verify_key_pem("MbedTLS.jl", signature)
end

# Basic TLS client functionality
let
    testhost = "httpbin.org"
    sock = connect(testhost, 443)
    entropy = MbedTLS.Entropy()

    function entropy_func(buf)
        buf[:] = rand(RandomDevice(), UInt8, length(buf))
        return Cint(length(buf))
    end

    MbedTLS.add_source!(entropy, entropy_func, 0, true)
    rng = MbedTLS.CtrDrbg()
    MbedTLS.seed!(rng, entropy)

    ctx = MbedTLS.SSLContext()
    conf = MbedTLS.SSLConfig()

    MbedTLS.config_defaults!(conf)
    MbedTLS.authmode!(conf, MbedTLS.MBEDTLS_SSL_VERIFY_REQUIRED)
    MbedTLS.rng!(conf, rng)

    function show_debug(level, filename, number, msg)
        @show level, filename, number, msg
    end

    MbedTLS.dbg!(conf, show_debug)
    MbedTLS.set_dbg_level(MbedTLS.DebugThreshold(1))

    MbedTLS.ca_chain!(conf)

    MbedTLS.setup!(ctx, conf)
    MbedTLS.set_bio!(ctx, sock)
    MbedTLS.hostname!(ctx, testhost)
    MbedTLS.handshake(ctx)

    write(ctx, "GET / HTTP/1.1\r\nHost: $testhost\r\n\r\n")
    buf = String(read(ctx, 100))
    @test occursin(r"^HTTP/1.1 200 OK", buf)
end

# Test ALPN
let
    testhost = "google.com"
    sock = connect(testhost, 443)
    entropy = MbedTLS.Entropy()

    function entropy_func(buf)
        buf[:] = rand(RandomDevice(), UInt8, length(buf))
        return Cint(length(buf))
    end

    MbedTLS.add_source!(entropy, entropy_func, 0, true)
    rng = MbedTLS.CtrDrbg()
    MbedTLS.seed!(rng, entropy)

    ctx = MbedTLS.SSLContext()
    conf = MbedTLS.SSLConfig()

    MbedTLS.config_defaults!(conf)
    MbedTLS.set_alpn!(conf, ["h2"])
    MbedTLS.authmode!(conf, MbedTLS.MBEDTLS_SSL_VERIFY_REQUIRED)
    MbedTLS.rng!(conf, rng)

    function show_debug(level, filename, number, msg)
        @show level, filename, number, msg
    end

    MbedTLS.dbg!(conf, show_debug)
    MbedTLS.set_dbg_level(MbedTLS.DebugThreshold(1))

    MbedTLS.ca_chain!(conf)

    MbedTLS.setup!(ctx, conf)
    MbedTLS.set_bio!(ctx, sock)
    MbedTLS.hostname!(ctx, testhost)
    MbedTLS.handshake(ctx)

    write(ctx, "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")
    @test MbedTLS.alpn_proto(ctx) == "h2"
end

# Test pk.jl methods
let
    key = MbedTLS.parse_keyfile(joinpath(@__DIR__, "key.pem"))
    @test MbedTLS.bitlength(key) == 2048
    @test MbedTLS.get_name(key) == "RSA"
    output = fill(0x00, 256)
    @test sizeof(output) == MbedTLS.sign!(key, MD_SHA1,
        MbedTLS.digest(MD_SHA1, "MbedTLS.jl"), output, MersenneTwister(0))
    verify_key_pem("MbedTLS.jl", output)

    signature = MbedTLS.sign(key, MD_SHA1,
        MbedTLS.digest(MD_SHA1, "MbedTLS.jl"), MersenneTwister(0))
    @test signature == output[1:length(signature)]

    key_string = read(open(joinpath(@__DIR__, "key.pem"), "r"))
    key = MbedTLS.PKContext()
    MbedTLS.parse_key!(key, key_string)
    @test MbedTLS.bitlength(key) == 2048
    @test MbedTLS.get_name(key) == "RSA"

    pubkey_string = read(open(joinpath(@__DIR__, "public_key.pem"), "r"))
    pubkey = MbedTLS.PKContext()
    MbedTLS.parse_public_key!(pubkey, pubkey_string)
    @test MbedTLS.bitlength(pubkey) == 2048
    @test MbedTLS.get_name(pubkey) == "RSA"

    key = MbedTLS.parse_keyfile(joinpath(@__DIR__, "key.pem"))
    @test MbedTLS.bitlength(key) == 2048
    @test MbedTLS.get_name(key) == "RSA"

    pubkey = MbedTLS.parse_public_keyfile(joinpath(@__DIR__, "public_key.pem"))
    @test MbedTLS.bitlength(pubkey) == 2048
    @test MbedTLS.get_name(pubkey) == "RSA"

    for md in instances(MbedTLS.MDKind)
        if in(md, (MD_NONE, MD_MD2, MD_MD4))
            continue
        end
        hash = MbedTLS.digest(md, "MbedTLS.jl")
        signature = MbedTLS.sign(key, md, hash, MersenneTwister(0))
        @test MbedTLS.verify(pubkey, md, hash, signature) == 0
    end
end

# Test md.jl
let
    md = MbedTLS.MD(MD_SHA1)
    write(md, UInt8['M', 'b', 'e', 'd'])
    a32 = fill(UInt32(0), 1)
    reinterpret(UInt8, a32) .= UInt8['T', 'L', 'S', '.']
    write(md, a32[])
    a16 = fill(UInt16(0), 1)
    reinterpret(UInt8, a16) .= UInt8['j', 'l']
    write(md, a16)
    @test MbedTLS.finish!(md) == MbedTLS.digest(MD_SHA1,b"MbedTLS.jl")

    # Test reset functionality
    md = MbedTLS.MD(MbedTLS.MD_SHA256, "passcode")
    write(md, "msg")
    digest1 = MbedTLS.finish!(md)
    MbedTLS.reset!(md)
    write(md, "msg")
    digest2 = MbedTLS.finish!(md)
    @test digest1 == digest2
    write(md, "msg")
    digest3 = MbedTLS.finish!(md)
    @test digest1 ≠ digest3
end

# log_secrets
mktempdir() do d

    f = joinpath(d, "secrets.log")

    testhost = "httpbin.org"
    sock = connect(testhost, 443)

    ctx = MbedTLS.SSLContext()
    conf = MbedTLS.SSLConfig(true; log_secrets=f)

    MbedTLS.setup!(ctx, conf)
    MbedTLS.set_bio!(ctx, sock)
    MbedTLS.hostname!(ctx, testhost)
    MbedTLS.handshake(ctx)

    write(ctx, "GET / HTTP/1.1\r\nHost: $testhost\r\n\r\n")
    buf = String(read(ctx, 100))
    @test occursin(r"^HTTP/1.1 200 OK", buf)
    @test occursin(r"^CLIENT_RANDOM [0-9a-f]{64} [0-9a-f]{96}$", read(f, String))
end

let
    include("clntsrvr/clntsrvr.jl")
end
