Initial commit with JWK parsing implementation.

This commit is contained in:
2021-11-22 15:33:59 -06:00
commit 0c53843e9b
16 changed files with 21457 additions and 0 deletions

3
src/main/jwt.nim Normal file
View File

@ -0,0 +1,3 @@
import jwt/jwk
export jwk

191
src/main/jwt/jwk.nim Normal file
View File

@ -0,0 +1,191 @@
import std/json, std/options, std/sequtils
import ../private/jsonutils
type
JwkKeyType* = enum EcPublic, EcPrivate, RsaPublic, RsaPrivate, Octet
JwkKty* = enum ktyEC = "EC", ktyRSA = "RSA", ktyOctet = "oct"
JwkUse* = enum jwkuSignature = "sig", jwkuEncrypt = "enc"
JwkOps* = enum jwkopSign = "sign", jwkopVerify = "verify",
jwkopEncrypt = "encrypt", jwkopDecrypt = "decrypt",
jwkopWrapKey = "wrapKey", jwkopUnwrapKey = "unwrapKey",
jwkopDeriveKey = "deriveKey", jwkopDeriveBits = "deriveBits"
JwkAlg* = enum
HS256 = "HS256", HS384 = "HS384", HS512 = "HS512",
RS256 = "RS256", RS384 = "RS384", RS512 = "RS512",
ES256 = "ES256", ES384 = "ES384", ES512 = "ES512",
PS256 = "PS256", PS384 = "PS384", PS512 = "PS512",
algNone = "none", algDir = "dir",
RSA1_5 = "RSA1_5", RSA_OAEP = "RSA-OAEP", RSA_OAEP_256 = "RSA-OEAP-256",
A128KW = "A128KW", A192KW = "A192KW", A256KW = "A256KW",
ECDH_ES = "ECDH_ES", ECDH_ES_A128KW = "ECDH_ES_A128KW",
ECDH_ES_A192KW = "ECDH_ES_A192KW", ECDH_ES_A256KW = "ECDH_ES_A256KW",
A128GCMKW = "A128GCMKW", A192GCMKW = "A192GCMKW", A256GCMKW = "A256GCMKW",
PBES2_HS256_A128KW = "PBES2_HS256_A128KW",
PBES2_HS384_A192KW = "PBES2_HS384_A192KW",
PBES2_HS512_A256KW = "PBES2_HS512_A256KW",
A128CBC_HS256 = "A128CBC_HS256",
A192CBC_HS384 = "A192CBC_HS384",
A256CBC_HS512 = "A256CBC_HS512",
A128GCM = "A128GCM", A192GCM = "A192GCM", A256GCM = "A256GCM"
EcCrv* = enum P256 = "P-256", P384 = "P-384", P521 = "P-521"
EcPubKey = object of RootObj
crv*: string
x*: string
y*: Option[string]
EcPrvKey* = object of EcPubKey
d*: string
RsaPubKey* = object of RootObj
n*: string
e*: string
RsaOtherPrime* = object
r*: string
d*: string
t*: string
RsaPrvKey* = object of RsaPubKey
d*: string
p*: Option[string]
q*: Option[string]
dp*: Option[string]
dq*: Option[string]
qi*: Option[string]
oth*: Option[seq[RsaOtherPrime]]
OctetKey* = object
k*: string
JWK* = object
json: JsonNode
kty*: string
use*: Option[string]
key_ops*: Option[string]
alg*: Option[string]
kid*: Option[string]
x5u*: Option[string]
x5c*: Option[string]
x5t*: Option[string]
x5tS256*: Option[string]
case keyKind*: JwkKeyType
of EcPublic: ecPub*: EcPubKey
of EcPrivate: ecPrv*: EcPrvKey
of RsaPublic: rsaPub*: RsaPubKey
of RsaPrivate: rsaPrv*: RsaPrvKey
of Octet: octKey*: OctetKey
JwkSet* = seq[JWK]
func `[]`*(jwk: JWK, key: string): JsonNode = jwk.json[key]
func `[]=`*(jwk: JWK, key: string, val: JsonNode): void = jwk.json[key] = val
func parseEcPubKey*(n: JsonNode): EcPubKey =
EcPubKey(
crv: n.reqStrVal("crv"),
x: n.reqStrVal("x"),
y: n.optStrVal("y"))
func parseEcPrvKey*(n: JsonNode): EcPrvKey =
let pk = parseEcPubKey(n)
EcPrvKey(crv: pk.crv, x: pk.x, y: pk.y, d: n.reqStrVal("d"))
func parseRsaPubKey*(n: JsonNode): RsaPubKey =
RsaPubKey(
n: n.reqStrVal("n"),
e: n.reqStrVal("e"))
func parseRsaOtherPrime(n: JsonNode): RsaOtherPrime =
RsaOtherPrime(
r: n.reqStrVal("r"),
d: n.reqStrVal("d"),
t: n.reqStrVal("t"))
func parseRsaPrvKey*(n: JsonNode): RsaPrvKey =
let pk = parseRsaPubKey(n)
RsaPrvKey(n: pk.n, e: pk.e,
d: n.reqStrVal("d"),
p: n.optStrVal("p"),
q: n.optStrVal("q"),
dp: n.optStrVal("dp"),
dq: n.optStrVal("dq"),
qi: n.optStrVal("qi"),
oth:
if n.hasKey("oth"):
some(n["oth"].getElems.mapIt(parseRsaOtherPrime(it)))
else: none[seq[RsaOtherPrime]]()
)
func parseOctetKey*(n: JsonNode): OctetKey =
OctetKey(k: n.reqStrVal("k"))
func parseJwk*(n: JsonNode): JWK =
# TODO: documentation, example, handle encrypted keys (JWEs)
case n["kty"].getStr(""):
of $ktyEC:
if n.hasKey("d"):
result = JWK(
kty: $ktyEC,
keyKind: EcPrivate,
ecPrv: parseEcPrvKey(n))
else:
result = JWK(
kty: $ktyEC,
keyKind: EcPublic,
ecPub: parseEcPubKey(n))
of $ktyRSA:
if n.hasKey("d"):
result = JWK(
kty: $ktyRSA,
keyKind: RsaPrivate,
rsaPrv: parseRsaPrvKey(n))
else:
result = JWK(
kty: $ktyRSA,
keyKind: RsaPublic,
rsaPub: parseRsaPubKey(n))
of $ktyOctet:
result = JWK(
kty: $ktyOctet,
keyKind: Octet,
octKey: parseOctetKey(n))
else: raise newException(ValueError,
"Unrecognized or missing kty: '" & n["kty"].getStr("") & "'")
result.json = n
result.use = n.optStrVal("use")
result.key_ops = n.optStrVal("key_ops")
result.alg = n.optStrVal("alg")
result.kid = n.optStrVal("kid")
result.x5u = n.optStrVal("x5u")
result.x5c = n.optStrVal("x5c")
result.x5t = n.optStrVal("x5t")
result.x5tS256 = n.optStrVal("x5t#S256")
func parseJwkSet*(n: JsonNode): JwkSet =
# TODO: documentation, examples, handled encrypted set (JWE)
if not n.hasKey("keys") or n["keys"].kind != JArray:
raise newException(ValueError, "JWK Set is missing the 'keys' member, " &
"or 'keys' is not an array.")
return n["keys"].getElems.mapIt(parseJwk(it))

View File

@ -0,0 +1,12 @@
import std/json, std/options, std/strutils
func reqStrVal*(n: JsonNode, key: string): string =
if n.hasKey(key):
let val = n[key].getStr("")
if not val.isEmptyOrWhitespace: return val
raise newException(ValueError, "missing value for '" & key & "'")
func optStrVal*(n: JsonNode, key: string): Option[string] =
if n.hasKey(key): some(n[key].getStr)
else: none[string]()

View File

@ -0,0 +1,2 @@
switch("path", "../../main")
switch("verbosity", "0")

3
src/test/unit/runner.nim Normal file
View File

@ -0,0 +1,3 @@
import std/unittest
import ./tjwk

217
src/test/unit/tjwk.nim Normal file
View File

@ -0,0 +1,217 @@
import std/json, std/options, std/unittest
import jwt/jwk
suite "jwt/jwk":
const rfc7517A1ExamplePubKeysStr = """
{"keys":
[
{"kty":"EC",
"crv":"P-256",
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use":"enc",
"kid":"1"},
{"kty":"RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"alg":"RS256",
"kid":"2011-04-29"}
]
}"""
let rfc7517A1ExamplePubKeysJson = parseJson(rfc7517A1ExamplePubKeysStr)
const rfc7517A2ExamplePrvKeysStr = """
{"keys":
[
{"kty":"EC",
"crv":"P-256",
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE",
"use":"enc",
"kid":"1"},
{"kty":"RSA",
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q",
"p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs",
"q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk",
"dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0",
"dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk",
"qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU",
"alg":"RS256",
"kid":"2011-04-29"}
]
}"""
let rfc7517A2ExamplePrvKeysJson = parseJson(rfc7517A2ExamplePrvKeysStr)
test "parseEcPubKey parses valid keys":
let keyNode = rfc7517A1ExamplePubKeysJson["keys"][0]
let jwk = parseJwk(keyNode)
check:
jwk.kty == $ktyEC
jwk.keyKind == EcPublic
jwk.use.isSome
jwk.use.get == $jwkuEncrypt
jwk.kid.isSome
jwk.kid.get == "1"
jwk.key_ops.isNone
jwk.ecPub.crv == $P256
jwk.ecPub.x == "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4"
jwk.ecPub.y.isSome
jwk.ecPub.y.get == "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"
test "parseEcPubKey rejects keys missing required values":
let keyNode = rfc7517A1ExamplePubKeysJson["keys"][0]
let withoutCrv = parseJson($keyNode)
withoutCrv.delete("crv")
expect ValueError: discard parseJwk(withoutCrv)
let withoutX = parseJson($keyNode)
withoutX.delete("x")
expect ValueError: discard parseJwk(withoutX)
let withoutY = parseJson($keyNode)
withoutY.delete("y")
let jwkWithoutY = parseJwk(withoutY)
check:
jwkWithoutY.kty == $ktyEc
jwkWithoutY.keyKind == EcPublic
jwkWithoutY.ecPub.y.isNone
test "parseRsaPubKey parses valid keys":
let keyNode = rfc7517A1ExamplePubKeysJson["keys"][1]
let jwk = parseJwk(keyNode)
check:
jwk.kty == $ktyRSA
jwk.keyKind == RsaPublic
jwk.alg.isSome
jwk.alg.get == $RS256
jwk.use.isNone
jwk.kid.isSome
jwk.kid.get == "2011-04-29"
jwk.key_ops.isNone
jwk.rsaPub.e == "AQAB"
jwk.rsaPub.n == "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw"
test "parseRsaPubKey rejects keys missing required values":
let keyNode = rfc7517A1ExamplePubKeysJson["keys"][1]
let withoutN = parseJson($keyNode)
withoutN.delete("n")
expect ValueError: discard parseJwk(withoutN)
let withoutE = parseJson($keyNode)
withoutE.delete("e")
expect ValueError: discard parseJwk(withoutE)
test "parseEcPrvKey parses valid keys":
let keyNode = rfc7517A2ExamplePrvKeysJson["keys"][0]
let jwk = parseJwk(keyNode)
check:
jwk.kty == $ktyEC
jwk.keyKind == EcPrivate
jwk.use.isSome
jwk.use.get == $jwkuEncrypt
jwk.kid.isSome
jwk.kid.get == "1"
jwk.key_ops.isNone
jwk.ecPrv.crv == $P256
jwk.ecPrv.x == "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4"
jwk.ecPrv.y.isSome
jwk.ecPrv.y.get == "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"
jwk.ecPrv.d == "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE"
test "parseEcPrvKey rejects keys missing required values":
let keyNode = rfc7517A2ExamplePrvKeysJson["keys"][0]
let withoutCrv = parseJson($keyNode)
withoutCrv.delete("crv")
expect ValueError: discard parseJwk(withoutCrv)
let withoutX = parseJson($keyNode)
withoutX.delete("x")
expect ValueError: discard parseJwk(withoutX)
let withoutY = parseJson($keyNode)
withoutY.delete("y")
let jwkWithoutY = parseJwk(withoutY)
check:
jwkWithoutY.kty == $ktyEc
jwkWithoutY.keyKind == EcPrivate
jwkWithoutY.ecPrv.y.isNone
test "parseRsaPrvKey parses valid keys":
let keyNode = rfc7517A2ExamplePrvKeysJson["keys"][1]
let jwk = parseJwk(keyNode)
check:
jwk.kty == $ktyRSA
jwk.keyKind == RsaPrivate
jwk.alg.isSome
jwk.alg.get == $RS256
jwk.use.isNone
jwk.kid.isSome
jwk.kid.get == "2011-04-29"
jwk.key_ops.isNone
jwk.rsaPrv.e == "AQAB"
jwk.rsaPrv.n == "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw"
jwk.rsaPrv.d == "X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q"
jwk.rsaPrv.p.isSome
jwk.rsaPrv.p.get == "83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs"
jwk.rsaPrv.q.isSome
jwk.rsaPrv.q.get == "3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk"
jwk.rsaPrv.dp.isSome
jwk.rsaPrv.dp.get == "G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0"
jwk.rsaPrv.dq.isSome
jwk.rsaPrv.dq.get == "s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk"
jwk.rsaPrv.qi.isSome
jwk.rsaPrv.qi.get == "GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU"
jwk.rsaPrv.oth.isNone
test "parseRsaPrvKey rejects keys missing required values":
let keyNode = rfc7517A2ExamplePrvKeysJson["keys"][1]
let withoutN = parseJson($keyNode)
withoutN.delete("n")
expect ValueError: discard parseJwk(withoutN)
let withoutE = parseJson($keyNode)
withoutE.delete("e")
expect ValueError: discard parseJwk(withoutE)
let withoutP = parseJson($keyNode)
withoutP.delete("p")
let jwkWithoutP = parseJwk(withoutP)
check jwkWithoutP.rsaPrv.p.isNone
test "parseJwkSet (public key examples)":
let jwkSet = parseJwkSet(rfc7517A1ExamplePubKeysJson)
check:
jwkSet.len == 2
jwkSet[0].kty == $ktyEC
jwkSet[0].keyKind == EcPublic
jwkSet[1].kty == $ktyRSA
jwkSet[1].keyKind == RsaPublic
test "parseJwkSet (private key examples)":
let jwkSet = parseJwkSet(rfc7517A2ExamplePrvKeysJson)
check:
jwkSet.len == 2
jwkSet[0].kty == $ktyEC
jwkSet[0].keyKind == EcPrivate
jwkSet[1].kty == $ktyRSA
jwkSet[1].keyKind == RsaPrivate