all repos — litestore @ 12a9933236aa423d673a74b618d8d1e381514a1d

A minimalist nosql document store.

src/litestorepkg/lib/jwt.nim

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
import std/[
    openssl, base64, strutils, macros, json, times, pegs, sequtils, os
    ]
import types, core

when defined(windows) and defined(amd64):
    {.passL: "-static -L"&getProjectPath()&"/litestorepkg/vendor/openssl/windows -lssl -lcrypto -lbcrypt".}
elif defined(linux) and defined(amd64):
    {.passL: "-static -L"&getProjectPath()&"/litestorepkg/vendor/openssl/linux -lssl -lcrypto".}
elif defined(macosx) and defined(amd64):
    {.passL: "-Bstatic -L"&getProjectPath()&"/litestorepkg/vendor/openssl/macosx -lssl -lcrypto -Bdynamic".}


proc EVP_PKEY_new(): EVP_PKEY {.cdecl, importc.}
proc X509_get_pubkey(cert: PX509): EVP_PKEY {.cdecl, importc.}
proc EVP_DigestVerifyInit(ctx: EVP_MD_CTX; pctx: ptr EVP_PKEY_CTX; typ: EVP_MD;
        e: ENGINE; pkey: EVP_PKEY): cint {.cdecl, importc.}
proc EVP_DigestVerifyUpdate(ctx: EVP_MD_CTX; data: pointer;
        len: cuint): cint {.cdecl, importc.}
proc EVP_DigestVerifyFinal(ctx: EVP_MD_CTX; data: pointer;
        len: cuint): cint {.cdecl, importc.}

proc getLastError(): string =
    return $ERR_error_string(ERR_get_error(), nil)

proc raiseJwtError(msg: string) =
    let err = getLastError()
    raise newException(EJwtValidationError, msg&"\n"&err)

proc raiseX509Error(msg: string) =
    let err = getLastError()
    raise newException(EX509Error, msg&"\n"&err)

proc getX5c*(LS: LiteStore; token: JWT): string =
    let keys = LS.jwks["keys"]
    if token.header.hasKey("kid"):
        let kid = token.header["kid"].getStr
        return keys.filterIt(it["kid"].getStr == kid)[0]["x5c"][0].getStr
    return keys[0]["x5c"][0].getStr

proc base64UrlDecode(encoded: string): string =
    let padding = 4 - (encoded.len mod 4)
    let base64String = encoded.replace("-", "+").replace("_", "/") & repeat("=", padding)
    result = base64.decode(base64String)

proc newJwt*(token: string): JWT =
    let parts = token.split(".")
    result.token = token
    result.payload = parts[0]&"."&parts[1]
    result.header = parts[0].base64UrlDecode.parseJson
    result.claims = parts[1].base64UrlDecode.parseJson
    result.signature = parts[2].base64UrlDecode

proc verifyTimeClaims*(jwt: JWT) =
    let t = now().toTime.toUnix
    if jwt.claims.hasKey("nbf") and jwt.claims["nbf"].getInt > t:
        raiseJwtError("Token cannot be used yet.")
    if jwt.claims.hasKey("exp") and jwt.claims["exp"].getInt < t:
        raiseJwtError("Token has expired.")

proc verifyAlgorithm*(jwt: JWT) =
    let alg = jwt.header["alg"].getStr
    if alg != "RS256":
        raiseJwtError("Algorithm not supported: " & alg)

proc verifyScope*(jwt: JWT; reqScope: seq[string] = @[]) =
    if reqScope.len == 0:
        return
    var scp = newSeq[string](0)
    if jwt.claims.hasKey("scp"):
        scp = jwt.claims["scp"].getStr.split(peg"\s")
    elif jwt.claims.hasKey("scope"):
        scp = jwt.claims["scope"].getStr.split(peg"\s")
    if scp.len == 0:
        raiseJwtError("No scp or scope claim found in token")
    var authorized = ""
    for s in scp:
        for r in reqScope:
            if r == s:
                authorized = s
                break
    if authorized == "":
        raise newException(EUnauthorizedError, "Unauthorized")

proc verifySignature*(jwt: JWT; x5c: string) =
    let sig = jwt.signature
    let payload = jwt.payload
    let cert = x5c.decode
    var pkeyctx: EVP_PKEY_CTX
    var mdctx: EVP_MD_CTX
    let alg = EVP_sha256();
    var x509: PX509
    var pubkey = EVP_PKEY_new()
    try:
        ### Validate Signature (Only RS256 supported)
        x509 = d2i_X509(cert)
        if x509.isNil:
            raiseX509Error("Invalid X509 certificate")

        pubkey = X509_get_pubkey(x509)
        if pubkey.isNil:
            raiseX509Error("An error occurred while retrieving the public key")

        mdctx = EVP_MD_CTX_create()
        if mdctx.isNil:
            raiseX509Error("Unable to initialize MD CTX")

        pkeyctx = EVP_PKEY_CTX_new(pubkey, nil)
        if pkeyctx.isNil:
            raiseX509Error("Unable to initialize PKEY CTX")

        if EVP_DigestVerifyInit(mdctx, addr pkeyctx, alg, nil, pubkey) != 1:
            raiseJwtError("Unable to initialize digest verification")

        if EVP_DigestVerifyUpdate(mdctx, addr payload[0], payload.len.cuint) != 1:
            raiseJwtError("Unable to update digest verification")

        if EVP_DigestVerifyFinal(mdctx, addr sig[0], sig.len.cuint) != 1:
            raiseJwtError("Verification failed")
    except CatchableError:
        let err = getCurrentException()
        if not mdctx.isNil:
            EVP_MD_CTX_destroy(mdctx)
        if not pkeyctx.isNil:
            EVP_PKEY_CTX_free(pkeyctx)
        if not pubkey.isNil:
            EVP_PKEY_free(pubkey)
        if not x509.isNil:
            X509_free(x509)
        raise err


when isMainModule:

    let token = "token.txt".readFile
    let x5c = "x5c.cert".readFile
    let jwt = token.newJwt

    echo token
    echo "---"
    echo x5c
    jwt.verifySignature(x5c)