5 Commits
15 changed files with 311 additions and 87 deletions

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
# Changelog
## 0.5.1 - 2026-10-05
### Changed
- Require BearSSL 0.3.0 or newer and use its managed PEM decoder interface.
## 0.5.0 - 2026-09-29
### Added
- Parsed `JWK` and `JwkSet` values now own their key material and metadata
allowing copies can be safely passed to worker threads without sharing
mutable JSON state.
- Added `%jwk` serialization, which produces an independent `JsonNode` and
retains extension members.
- The unit-test task now runs under threaded ARC and ORC memory management, with
coverage for copied key caches used concurrently by workers.
### Changed
- `initJwk` now snapshots its input JSON. Mutating the source `JsonNode` after
parsing no longer changes the resulting key.
- `jwk["member"]` still returns `Option[JsonNode]`, but the node is now a fresh
copy. Mutating it no longer changes the key; construct a new key with
`initJwk` to make a modification.
+24
View File
@@ -4,6 +4,30 @@ This is a standards-compliant implementation of the JWT, JWK, JWS, and JWE
standards in Nim, using the [BearSSL](https://www.bearssl.org/) library to standards in Nim, using the [BearSSL](https://www.bearssl.org/) library to
provide the underlying cryptographic primatives. provide the underlying cryptographic primatives.
## JWK ownership and threads
`JWK` and `JwkSet` have value semantics. Parsed keys own their key material and
metadata; changing the input `JsonNode` after `initJwk` or `initJwkSet` does not
change the keys. Assigning a key, key set, or `Table[string, JwkSet]` produces an
independent value suitable for handing to a worker thread. Synchronize access
to any source value that another thread may be modifying.
The typed accessors (`kid`, `alg`, `rsaPrv`, etc.) return values. The JSON member
accessor, `key["member"]`, keeps its `Option[JsonNode]` return type, but each call
returns a fresh node. Mutating that node does not update the key; construct a
new key with `initJwk` to change it. Missing members return `none`, while an
explicit JSON null returns `some` containing a `JNull` node.
Use `%key` to serialize a complete key, including extension members, to an
independent JSON object.
Internally, arbitrary JSON members are stored as encoded strings in a value
table. Only JSON member lookups decode them. Signing and verification use the
parsed key fields directly. This ownership guarantee applies to `JWK` and
`JwkSet`; JWT claims and JOSE headers retain their existing ownership behavior.
Run `nimble unittest` to run the suite, including worker-thread tests, under
both ARC and ORC.
## Supported Algorithms (JWA) ## Supported Algorithms (JWA)
[RFC7518 (JWA)][rfc7518] registers [RFC7518 (JWA)][rfc7518] registers
+4 -3
View File
@@ -1,6 +1,6 @@
# Package # Package
version = "0.1.0" version = "0.5.1"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Full JWT, JWS, JWE, and JWK implementation for Nim, compliant with RFCs 7515-7519." description = "Full JWT, JWS, JWE, and JWK implementation for Nim, compliant with RFCs 7515-7519."
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
@@ -9,8 +9,9 @@ srcDir = "src/main"
# Dependencies # Dependencies
requires "nim >= 1.4.8" requires "nim >= 1.4.8"
requires "bearssl" requires "bearssl >= 0.3.0"
requires "https://github.com/yglukhov/bearssl_pkey_decoder" requires "https://github.com/yglukhov/bearssl_pkey_decoder"
task unittest, "Run the unit test suites.": task unittest, "Run the unit test suites.":
exec "nim c -r src/test/unit/runner" exec "nim c -r --threads:on --gc:arc src/test/unit/runner"
exec "nim c -r --threads:on --gc:orc src/test/unit/runner"
+26 -16
View File
@@ -25,25 +25,29 @@ type
JwtClaims* = object JwtClaims* = object
rawB64: string rawB64: string
json: JsonNode json: JsonNode
iss: Option[string]
sub: Option[string]
aud: Option[string] aud: Option[string]
exp: Option[DateTime] exp: Option[DateTime]
nbf: Option[DateTime]
iat: Option[DateTime] iat: Option[DateTime]
iss: Option[string]
jti: Option[string] jti: Option[string]
nbf: Option[DateTime]
sid: Option[string]
sub: Option[string]
typ: Option[string]
# Public read-only accessors to JwtClaims members # Public read-only accessors to JwtClaims members
# ----------------------------------------------- # -----------------------------------------------
func rawB64*(c: JwtClaims): string = c.rawB64 func rawB64*(c: JwtClaims): string = c.rawB64
func iss*(c: JwtClaims): Option[string] = c.iss
func sub*(c: JwtClaims): Option[string] = c.sub
func aud*(c: JwtClaims): Option[string] = c.aud func aud*(c: JwtClaims): Option[string] = c.aud
func exp*(c: JwtClaims): Option[DateTime] = c.exp func exp*(c: JwtClaims): Option[DateTime] = c.exp
func nbf*(c: JwtClaims): Option[DateTime] = c.nbf
func iat*(c: JwtClaims): Option[DateTime] = c.iat func iat*(c: JwtClaims): Option[DateTime] = c.iat
func iss*(c: JwtClaims): Option[string] = c.iss
func jti*(c: JwtClaims): Option[string] = c.jti func jti*(c: JwtClaims): Option[string] = c.jti
func nbf*(c: JwtClaims): Option[DateTime] = c.nbf
func sid*(c: JwtClaims): Option[string] = c.sid
func sub*(c: JwtClaims): Option[string] = c.sub
func typ*(c: JwtClaims): Option[string] = c.typ
func `[]`*(c: JwtClaims, key: string): Option[JsonNode] = func `[]`*(c: JwtClaims, key: string): Option[JsonNode] =
## Generic accessor to claim values by name ## Generic accessor to claim values by name
@@ -61,13 +65,15 @@ proc initJwtClaims*(n: JsonNode): JwtClaims =
return JwtClaims( return JwtClaims(
rawB64: b64UrlEncode($n), rawB64: b64UrlEncode($n),
json: n, json: n,
iss: n.optStrVal("iss"),
sub: n.optStrVal("sub"),
aud: n.optStrVal("aud"), aud: n.optStrVal("aud"),
exp: n.optNumericDate("exp"), exp: n.optNumericDate("exp"),
nbf: n.optNumericDate("nbf"),
iat: n.optNumericDate("iat"), iat: n.optNumericDate("iat"),
jti: n.optStrVal("jtu")) iss: n.optStrVal("iss"),
jti: n.optStrVal("jtu"),
nbf: n.optNumericDate("nbf"),
sid: n.optStrVal("sid"),
sub: n.optStrVal("sub"),
typ: n.optStrVal("typ"))
proc initJwtClaims*(encoded: string): JwtClaims = proc initJwtClaims*(encoded: string): JwtClaims =
## Parse a Base64url-encoded set of claims into a JwtClaims object ## Parse a Base64url-encoded set of claims into a JwtClaims object
@@ -77,13 +83,15 @@ proc initJwtClaims*(encoded: string): JwtClaims =
result.rawB64 = encoded result.rawB64 = encoded
proc initJwtClaims*( proc initJwtClaims*(
iss: Option[string] = none[string](),
sub: Option[string] = none[string](),
aud: Option[string] = none[string](), aud: Option[string] = none[string](),
exp: Option[DateTime] = none[DateTime](), exp: Option[DateTime] = none[DateTime](),
nbf: Option[DateTime] = none[DateTime](),
iat: Option[DateTime] = none[DateTime](), iat: Option[DateTime] = none[DateTime](),
iss: Option[string] = none[string](),
jti: Option[string] = none[string](), jti: Option[string] = none[string](),
nbf: Option[DateTime] = none[DateTime](),
sid: Option[string] = none[string](),
sub: Option[string] = none[string](),
typ: Option[string] = none[string](),
moreClaims: seq[tuple[k, v: string]] = @[] moreClaims: seq[tuple[k, v: string]] = @[]
): JwtClaims = ): JwtClaims =
## Convenience method to initialize a new JwtClaims object by specifying ## Convenience method to initialize a new JwtClaims object by specifying
@@ -99,12 +107,14 @@ proc initJwtClaims*(
let json = newJObject() let json = newJObject()
for claim in moreClaims: json[claim.k] = %claim.v for claim in moreClaims: json[claim.k] = %claim.v
if iss.isSome: json["iss"] = %iss.get
if sub.isSome: json["sub"] = %sub.get
if aud.isSome: json["aud"] = %aud.get if aud.isSome: json["aud"] = %aud.get
if exp.isSome: json["exp"] = %toNumericDate(exp.get) if exp.isSome: json["exp"] = %toNumericDate(exp.get)
if nbf.isSome: json["nbf"] = %toNumericDate(nbf.get)
if iat.isSome: json["iat"] = %toNumericDate(iat.get) if iat.isSome: json["iat"] = %toNumericDate(iat.get)
if iss.isSome: json["iss"] = %iss.get
if jti.isSome: json["jti"] = %jti.get if jti.isSome: json["jti"] = %jti.get
if nbf.isSome: json["nbf"] = %toNumericDate(nbf.get)
if sid.isSome: json["sid"] = %sid.get
if sub.isSome: json["sub"] = %sub.get
if typ.isSome: json["typ"] = %typ.get
return initJwtClaims(json) return initJwtClaims(json)
+24 -6
View File
@@ -11,7 +11,7 @@
## ##
## Nim implementation of the data structures and constants defined in RFC 7518. ## Nim implementation of the data structures and constants defined in RFC 7518.
import std/json, std/options, std/sequtils, std/strutils import std/json, std/options, std/sequtils, std/strutils, std/tables
import ./jwa import ./jwa
import ./private/jsonutils import ./private/jsonutils
@@ -56,7 +56,12 @@ type
k*: string k*: string
JWK* = object JWK* = object
json: JsonNode ## Owns its key material and metadata with value semantics. Copies may be
## handed to separate threads; synchronize access to a source being mutated.
## JSON member access returns a fresh node, independent of the key.
jsonMembers: Table[string, string]
# Keep arbitrary JSON members as encoded values, without JsonNode refs.
# Cryptographic operations use the parsed fields below, without decoding.
kty: string kty: string
use: Option[string] use: Option[string]
key_ops: Option[string] key_ops: Option[string]
@@ -96,8 +101,18 @@ func rsaPrv*(jwk: JWK): RsaPrvKey = jwk.rsaPrv
func octKey*(jwk: JWK): OctetKey = jwk.octKey func octKey*(jwk: JWK): OctetKey = jwk.octKey
func `[]`*(jwk: JWK, key: string): Option[JsonNode] = func `[]`*(jwk: JWK, key: string): Option[JsonNode] =
if jwk.json.hasKey(key): some(jwk.json[key]) ## Return an independent JSON value for a member, or none if it is absent.
else: none[JsonNode]() if jwk.jsonMembers.hasKey(key):
# The string parser only touches a newly allocated in-memory stream, but
# its stream-based implementation is not annotated noSideEffect in std/json.
{.cast(noSideEffect).}:
result = some(parseJson(jwk.jsonMembers[key]))
func `%`*(jwk: JWK): JsonNode =
## Serialize the key and its extension members to an independent JSON object.
result = newJObject()
for name in jwk.jsonMembers.keys:
result[name] = jwk[name].get
# Public Parsing Functions # Public Parsing Functions
# ------------------------ # ------------------------
@@ -143,7 +158,9 @@ func parseOctetKey*(n: JsonNode): OctetKey =
OctetKey(k: n.reqStrVal("k")) OctetKey(k: n.reqStrVal("k"))
func initJwk*(n: JsonNode): JWK = func initJwk*(n: JsonNode): JWK =
# TODO: documentation, example, handle encrypted keys (JWEs) ## Parse an owned snapshot of a key, including its extension metadata.
## Later changes to n or to nodes returned by [] do not affect the key.
# TODO: example, handle encrypted keys (JWEs)
case n["kty"].getStr(""): case n["kty"].getStr(""):
of $ktyEC: of $ktyEC:
@@ -179,7 +196,8 @@ func initJwk*(n: JsonNode): JWK =
else: raise newException(ValueError, else: raise newException(ValueError,
"Unrecognized or missing kty: '" & n["kty"].getStr("") & "'") "Unrecognized or missing kty: '" & n["kty"].getStr("") & "'")
result.json = n for name, value in n.pairs:
result.jsonMembers[name] = $value
result.use = n.optStrVal("use") result.use = n.optStrVal("use")
result.key_ops = n.optStrVal("key_ops") result.key_ops = n.optStrVal("key_ops")
result.alg = n.optStrVal("alg") result.alg = n.optStrVal("alg")
+2 -2
View File
@@ -1,5 +1,5 @@
# jwt_full/jwt.nim # jwt_full/jwt.nim
# Copyright 2021 Jonathan Bernard # Copyright © 2021 Jonathan Bernard
## =============================== ## ===============================
## jwt ## jwt
@@ -57,7 +57,7 @@ proc initJWT*(encoded: string): JWT =
jws: jws) jws: jws)
else: else:
# TODO # TODO
raise newException(Exception, "not yet implemented") raise newException(Exception, "partial JWT construction is not yet implemented")
proc toJWT*(encoded: string): JWT = initJWT(encoded) proc toJWT*(encoded: string): JWT = initJWT(encoded)
+11 -12
View File
@@ -1,4 +1,3 @@
import std/tables
import bearssl, bearssl_pkey_decoder import bearssl, bearssl_pkey_decoder
import ../../encoding import ../../encoding
@@ -35,14 +34,14 @@ func fromBearSslCurveConst(curve: int32): EcCurve =
func initEcPublicKeyObj(curve: EcCurve, q: string): EcPublicKeyObj = func initEcPublicKeyObj(curve: EcCurve, q: string): EcPublicKeyObj =
result = EcPublicKeyObj(curve: curve, q: q) result = EcPublicKeyObj(curve: curve, q: q)
result.bearKey.curve = curve.toBearSslCurveConst result.bearKey.curve = curve.toBearSslCurveConst
result.bearKey.q = cast[ptr char](result.q.cstring) result.bearKey.q = cast[ptr byte](result.q.cstring)
result.bearKey.qlen = q.len result.bearKey.qlen = uint q.len
func initEcPrivateKeyObj(curve: EcCurve, x: string): EcPrivateKeyObj = func initEcPrivateKeyObj(curve: EcCurve, x: string): EcPrivateKeyObj =
result = EcPrivateKeyObj(curve: curve, x: x) result = EcPrivateKeyObj(curve: curve, x: x)
result.bearKey.curve = curve.toBearSslCurveConst result.bearKey.curve = curve.toBearSslCurveConst
result.bearKey.x = cast[ptr char](result.x.cstring) result.bearKey.x = cast[ptr byte](result.x.cstring)
result.bearKey.xlen = x.len result.bearKey.xlen = uint x.len
proc toEcPublicKey(jwk: JWK): EcPublicKeyObj = proc toEcPublicKey(jwk: JWK): EcPublicKeyObj =
## Convert an ECDSA public key in JWK format to the wrapper for BearSSL's ## Convert an ECDSA public key in JWK format to the wrapper for BearSSL's
@@ -66,7 +65,7 @@ proc toEcPublicKey(pem: string): EcPublicKeyObj =
cast[BearPemDecoderCallback](pkeyDecoderPush)) cast[BearPemDecoderCallback](pkeyDecoderPush))
let k = pkCtx.key.ec let k = pkCtx.key.ec
return initEcPublicKeyObj(k.curve.fromBearSslCurveConst, $cstring(k.q)) return initEcPublicKeyObj(k.curve.fromBearSslCurveConst, $cast[cstring](k.q))
proc toEcPrivateKey(jwk: JWK): EcPrivateKeyObj = proc toEcPrivateKey(jwk: JWK): EcPrivateKeyObj =
## Convert an ECDSA private key in JWK format to the wrapper for BearSSL's ## Convert an ECDSA private key in JWK format to the wrapper for BearSSL's
@@ -81,12 +80,12 @@ proc toEcPrivateKey(jwk: JWK): EcPrivateKeyObj =
proc toEcPrivateKey(pem: string): EcPrivateKeyObj = proc toEcPrivateKey(pem: string): EcPrivateKeyObj =
var skCtx: SkeyDecoderContext var skCtx: SkeyDecoderContext
skeyDecoderInit(addr skCtx) skeyDecoderInit(skCtx)
decodePem(pem, "EC PRIVATE KEY", addr skCtx, decodePem(pem, "EC PRIVATE KEY", addr skCtx,
cast[BearPemDecoderCallback](skeyDecoderPush)) cast[BearPemDecoderCallback](skeyDecoderPush))
let k = skCtx.key.ec let k = skCtx.key.ec
return initEcPrivateKeyObj(k.curve.fromBearSslCurveConst, $cstring(k.x)) return initEcPrivateKeyObj(k.curve.fromBearSslCurveConst, $cast[cstring](k.x))
proc getEcHashCfg(alg: JwtAlgorithm): HashCfg = proc getEcHashCfg(alg: JwtAlgorithm): HashCfg =
let hashAlg = case alg: let hashAlg = case alg:
@@ -137,11 +136,11 @@ proc bearEcVerify(
let ecVerifyImpl = ecdsaVrfyRawGetDefault() let ecVerifyImpl = ecdsaVrfyRawGetDefault()
let resultCode = ecVerifyImpl( let resultCode = ecVerifyImpl(
addr ecAllM15, addr ecAllM15,
cast[ptr char](unsafeAddr hashed[0]), hashed.cstring,
hashed.len, uint hashed.len,
unsafeAddr key.bearKey, unsafeAddr key.bearKey,
cast[ptr char](unsafeAddr signature[0]), signature.cstring,
signature.len) uint signature.len)
result = resultCode == 1 result = resultCode == 1
+2 -2
View File
@@ -66,5 +66,5 @@ proc hash*(data: string, alg: HashAlgorithm): string =
let hashCfg = hashConfig(alg) let hashCfg = hashConfig(alg)
result = newString(hashCfg.size) result = newString(hashCfg.size)
hashCfg.vtable.init(pCtx) hashCfg.vtable.init(pCtx)
hashCfg.vtable.update(pCtx, unsafeAddr data[0], data.len) hashCfg.vtable.update(pCtx, data.cstring, uint data.len)
hashCfg.vtable.output(pCtx, addr result[0]) hashCfg.vtable.out(pCtx, addr result[0])
+5 -5
View File
@@ -26,13 +26,13 @@ proc bearHMAC(message: string, alg: JwtAlgorithm, key: string): string =
var keyCtx: HmacKeyContext var keyCtx: HmacKeyContext
var hmacCtx: HmacContext var hmacCtx: HmacContext
hmacKeyInit(addr keyCtx, vtable, key.cstring, key.len) hmacKeyInit(keyCtx, vtable, key.cstring, uint key.len)
hmacInit(addr hmacCtx, addr keyCtx, 0) hmacInit(hmacCtx, keyCtx, uint 0)
hmacUpdate(addr hmacCtx, message.cstring, message.len) hmacUpdate(hmacCtx, message.cstring, uint message.len)
let resLen = hmacSize(addr hmacCtx) let resLen = hmacSize(hmacCtx)
result = newString(resLen) result = newString(resLen)
discard hmacOut(addr hmacCtx, addr result[0]) discard hmacOut(hmacCtx, addr result[0])
proc hmac*(message: string, alg: JwtAlgorithm, key: string): string = proc hmac*(message: string, alg: JwtAlgorithm, key: string): string =
return bearHMAC(message, alg, key) return bearHMAC(message, alg, key)
+8 -10
View File
@@ -3,7 +3,7 @@ import bearssl
# Taken from nim-bearssl/decls.nim # Taken from nim-bearssl/decls.nim
{.pragma: bearSslFunc, cdecl, gcsafe, noSideEffect, raises: [].} {.pragma: bearSslFunc, cdecl, gcsafe, noSideEffect, raises: [].}
type BearPemDecoderCallback* = proc(keyCtx: pointer, data: pointer, dataLen: int) {.bearSslFunc.} type BearPemDecoderCallback* = proc(destCtx: pointer, src: pointer, dataLen: csize_t) {.bearSslFunc.}
proc decodePem*( proc decodePem*(
pem: string, pem: string,
@@ -13,30 +13,28 @@ proc decodePem*(
) = ) =
var pemCtx: PemDecoderContext var pemCtx: PemDecoderContext
pemDecoderInit(addr pemCtx) pemCtx.init()
var offset = 0 var offset = 0
var bytesRemaining = len(pem)
var readingObj = false var readingObj = false
while bytesRemaining > 0: while offset < pem.len:
let bytesRead = pemDecoderPush(addr pemCtx, unsafeAddr pem[offset], bytesRemaining) let bytesRead = pemCtx.push(pem.toOpenArray(offset, pem.high))
offset += bytesRead offset += bytesRead
bytesRemaining -= bytesRead
case pemDecoderEvent(addr pemCtx): case pemCtx.lastEvent():
of PEM_BEGIN_OBJ: of PEM_BEGIN_OBJ:
if readingObj: if readingObj:
raise newException(ValueError, raise newException(ValueError,
"Invalid PEM: saw a second BEGIN before seeing END.") "Invalid PEM: saw a second BEGIN before seeing END.")
if pemDecoderName(addr pemCtx) != expectedObjectName: if pemCtx.banner() != expectedObjectName:
raise newException(ValueError, raise newException(ValueError,
"Invalid PEM: expected BEGIN " & expectedObjectName & "Invalid PEM: expected BEGIN " & expectedObjectName &
" but got BEGIN " & $pemDecoderName(addr pemCtx)) " but got BEGIN " & pemCtx.banner())
readingObj = true readingObj = true
pemDecoderSetdest(addr pemCtx, callback, keyCtx) pemCtx.setdest(callback, keyCtx)
of PEM_END_OBJ: of PEM_END_OBJ:
if readingObj: readingObj = false if readingObj: readingObj = false
+30 -30
View File
@@ -19,24 +19,24 @@ type
func initRsaPublicKeyObj(n, e: string): RsaPublicKeyObj = func initRsaPublicKeyObj(n, e: string): RsaPublicKeyObj =
result = RsaPublicKeyObj(n: n, e: e) result = RsaPublicKeyObj(n: n, e: e)
result.bearKey.n = cast[ptr char](result.n.cstring) result.bearKey.n = cast[ptr byte](result.n.cstring)
result.bearKey.nlen = result.n.len result.bearKey.nlen = uint result.n.len
result.bearKey.e = cast[ptr char](result.e.cstring) result.bearKey.e = cast[ptr byte](result.e.cstring)
result.bearKey.elen = result.e.len result.bearKey.elen = uint result.e.len
func initRsaPrivateKeyObj(nBitLen: int, p, q, dp, dq, iq: string): RsaPrivateKeyObj = func initRsaPrivateKeyObj(nBitLen: int, p, q, dp, dq, iq: string): RsaPrivateKeyObj =
result = RsaPrivateKeyObj(p: p, q: q, dp: dp, dq: dq, iq: iq) result = RsaPrivateKeyObj(p: p, q: q, dp: dp, dq: dq, iq: iq)
result.bearKey.nBitLen = cast[uint32](nBitLen) result.bearKey.nBitLen = cast[uint32](nBitLen)
result.bearKey.p = cast[ptr char](result.p.cstring) result.bearKey.p = cast[ptr byte](result.p.cstring)
result.bearKey.plen = result.p.len result.bearKey.plen = uint result.p.len
result.bearKey.q = cast[ptr char](result.q.cstring) result.bearKey.q = cast[ptr byte](result.q.cstring)
result.bearKey.qlen = result.q.len result.bearKey.qlen = uint result.q.len
result.bearKey.dp = cast[ptr char](result.dp.cstring) result.bearKey.dp = cast[ptr byte](result.dp.cstring)
result.bearKey.dplen = result.dp.len result.bearKey.dplen = uint result.dp.len
result.bearKey.dq = cast[ptr char](result.dq.cstring) result.bearKey.dq = cast[ptr byte](result.dq.cstring)
result.bearKey.dqlen = result.dq.len result.bearKey.dqlen = uint result.dq.len
result.bearKey.iq = cast[ptr char](result.iq.cstring) result.bearKey.iq = cast[ptr byte](result.iq.cstring)
result.bearKey.iqlen = result.iq.len result.bearKey.iqlen = uint result.iq.len
proc toRsaPublicKey(jwk: JWK): RsaPublicKeyObj = proc toRsaPublicKey(jwk: JWK): RsaPublicKeyObj =
## Convert an RSA public key in JWK format to the wrapper for BearSSL's ## Convert an RSA public key in JWK format to the wrapper for BearSSL's
@@ -60,7 +60,7 @@ proc toRsaPublicKey(pem: string): RsaPublicKeyObj =
cast[BearPemDecoderCallback](pkeyDecoderPush)) cast[BearPemDecoderCallback](pkeyDecoderPush))
let k = pkCtx.key.rsa let k = pkCtx.key.rsa
return initRsaPublicKeyObj($cstring(k.n), $cstring(k.e)) return initRsaPublicKeyObj($cast[cstring](k.n), $cast[cstring](k.e))
proc toRsaPrivateKey(jwk: JWK): RsaPrivateKeyObj = proc toRsaPrivateKey(jwk: JWK): RsaPrivateKeyObj =
@@ -111,7 +111,7 @@ proc toRsaPrivateKey(jwk: JWK): RsaPrivateKeyObj =
proc toRsaPrivateKey(pem: string): RsaPrivateKeyObj = proc toRsaPrivateKey(pem: string): RsaPrivateKeyObj =
var skCtx: SkeyDecoderContext var skCtx: SkeyDecoderContext
skeyDecoderInit(addr skCtx) skeyDecoderInit(skCtx)
decodePem(pem, "RSA PRIVATE KEY", addr skCtx, decodePem(pem, "RSA PRIVATE KEY", addr skCtx,
cast[BearPemDecoderCallback](skeyDecoderPush)) cast[BearPemDecoderCallback](skeyDecoderPush))
@@ -119,11 +119,11 @@ proc toRsaPrivateKey(pem: string): RsaPrivateKeyObj =
return initRsaPrivateKeyObj( return initRsaPrivateKeyObj(
cast[int](k.nBitLen), cast[int](k.nBitLen),
$cstring(k.p), $cast[cstring](k.p),
$cstring(k.q), $cast[cstring](k.q),
$cstring(k.dp), $cast[cstring](k.dp),
$cstring(k.dq), $cast[cstring](k.dq),
$cstring(k.iq)) $cast[cstring](k.iq))
proc getRsaHashCfg(alg: JwtAlgorithm): HashCfg = proc getRsaHashCfg(alg: JwtAlgorithm): HashCfg =
let hashAlg = case alg: let hashAlg = case alg:
@@ -148,11 +148,11 @@ proc bearRsaSign(
result = newString((key.bearKey.nBitLen + 7) div 8) result = newString((key.bearKey.nBitLen + 7) div 8)
let errCode = rsaSignImpl( let errCode = rsaSignImpl(
cast[ptr char](hashCfg.oid), cast[ptr byte](hashCfg.oid),
cast[ptr char](unsafeAddr hashed[0]), cast[ptr byte](unsafeAddr hashed[0]),
hashed.len, uint hashed.len,
unsafeAddr key.bearKey, unsafeAddr key.bearKey,
cast[ptr char](addr result[0])) cast[ptr byte](addr result[0]))
if errCode != 1: raise newException(Exception, "RSA signature failed") if errCode != 1: raise newException(Exception, "RSA signature failed")
@@ -169,12 +169,12 @@ proc bearRsaVerify(
var recoveredHash = newString(hashCfg.size) var recoveredHash = newString(hashCfg.size)
let errCode = rsaVerifyImpl( let errCode = rsaVerifyImpl(
cast[ptr char](unsafeAddr signature[0]), cast[ptr byte](unsafeAddr signature[0]),
signature.len, uint signature.len,
cast[ptr char](hashCfg.oid), cast[ptr byte](hashCfg.oid),
hashed.len, uint hashed.len,
unsafeAddr key.bearKey, unsafeAddr key.bearKey,
cast[ptr char](addr recoveredHash[0])) cast[ptr byte](addr recoveredHash[0]))
if errCode != 1: return false if errCode != 1: return false
return hashed == recoveredHash return hashed == recoveredHash
+1
View File
@@ -4,5 +4,6 @@ import ./tclaims
import ./tencoding import ./tencoding
import ./tjoseheader import ./tjoseheader
import ./tjwk import ./tjwk
import ./tjwkthreads
import ./tjws import ./tjws
import ./tjwt import ./tjwt
+14
View File
@@ -2,6 +2,7 @@ import std/json, std/options, std/unittest
import jwt_full/jwa import jwt_full/jwa
import jwt_full/joseheader import jwt_full/joseheader
import jwt_full/jwk
import ../testdata import ../testdata
@@ -16,3 +17,16 @@ suite "jwt_full/joseheader":
h1.rawB64 == rfc7519_S31_HeaderB64 h1.rawB64 == rfc7519_S31_HeaderB64
h2.rawB64 == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" h2.rawB64 == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"
h3.rawB64 == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" h3.rawB64 == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
test "initJoseHeader serializes an embedded JWK":
let node = %*{"kty": "oct", "k": "c2VjcmV0", "kid": "original",
"metadata": {"labels": ["original"]}}
let key = initJwk(node)
let header = initJoseHeader(jwk = some(key))
check header["jwk"].get == node
check header.jwk.get.kid.get == "original"
let decoded = initJoseHeader(header.rawB64)
check decoded["jwk"].get == node
header["jwk"].get["metadata"]["labels"][0].str = "changed"
check header.jwk.get["metadata"].get == node["metadata"]
check key["metadata"].get == node["metadata"]
+86 -1
View File
@@ -1,4 +1,4 @@
import std/json, std/options, std/unittest import std/json, std/options, std/tables, std/unittest
import jwt_full/jwa, jwt_full/jwk import jwt_full/jwa, jwt_full/jwk
@@ -215,3 +215,88 @@ suite "jwt_full/jwk":
jwkSet[0].keyKind == EcPrivate jwkSet[0].keyKind == EcPrivate
jwkSet[1].kty == $ktyRSA jwkSet[1].kty == $ktyRSA
jwkSet[1].keyKind == RsaPrivate jwkSet[1].keyKind == RsaPrivate
test "all key variants own their input JSON":
let nodes = @[
rfc7517A1ExamplePubKeysJson["keys"][0].copy,
rfc7517A1ExamplePubKeysJson["keys"][1].copy,
rfc7517A2ExamplePrvKeysJson["keys"][0].copy,
rfc7517A2ExamplePrvKeysJson["keys"][1].copy,
%*{"kty": "oct", "k": "c2VjcmV0", "kid": "octet"}]
for node in nodes:
node["metadata"] = %*{"labels": ["original"]}
let original = node.copy
let key = initJwk(node)
node["kid"].str = "changed"
node["metadata"]["labels"][0].str = "changed"
node["newMember"] = %true
for name, value in original.pairs:
check key[name].get == value
check key.kid.get == original["kid"].getStr
check key["newMember"].isNone
test "JSON member lookups return independent values":
let key = initJwk(%*{
"kty": "oct", "k": "c2VjcmV0", "kid": "original",
"metadata": {"labels": ["original"]}})
let copied = key
let metadata = copied["metadata"].get
metadata["labels"][0].str = "changed"
let kid = copied["kid"].get
kid.str = "changed"
check key["metadata"].get["labels"][0].getStr == "original"
check copied["metadata"].get["labels"][0].getStr == "original"
check key["kid"].get.getStr == "original"
check copied["kid"].get.getStr == "original"
check copied.kid.get == "original"
test "JSON serialization round trips without exposing owned state":
let node = %*{"kty": "oct", "k": "c2VjcmV0", "kid": "original",
"metadata": {"labels": ["original"]}}
let key = initJwk(node)
let serialized = %key
check serialized == node
check %initJwk(serialized) == node
serialized["metadata"]["labels"][0].str = "changed"
check %key == node
test "member lookup preserves JSON types and missing versus null":
let node = %*{
"kty": "oct", "k": "c2VjcmV0", "nullValue": nil,
"integer": 1234567890123456789'i64, "float": 1.25, "boolean": true,
"string": "quotes: \" newline: \n unicode: \u263a",
"array": [nil, false, 42, {"nested": []}], "object": {}}
let key = initJwk(node)
for name, value in node.pairs:
check key[name].get.kind == value.kind
check key[name].get == value
check key["nullValue"].isSome
check key["nullValue"].get.kind == JNull
check key["missing"].isNone
check default(JWK)["missing"].isNone
test "tables of key sets have independent copies":
let node = %*{"keys": [{"kty": "oct", "k": "c2VjcmV0", "kid": "original"}]}
let keys = initJwkSet(node)
let original = {"issuer": keys}.toTable
var copied = original
node["keys"][0]["kid"].str = "changed"
copied["issuer"][0]["kid"].get.str = "changed"
copied["issuer"].add initJwk(%*{"kty": "oct", "k": "c2VjcmV0", "kid": "added"})
check keys[0]["kid"].get.getStr == "original"
check original["issuer"].len == 1
check original["issuer"][0]["kid"].get.getStr == "original"
check copied["issuer"].len == 2
check copied["issuer"][0]["kid"].get.getStr == "original"
test "RSA accessors return independent nested value fields":
let node = rfc7517A2ExamplePrvKeysJson["keys"][1].copy
node["oth"] = %*[{"r": "Aw", "d": "BQ", "t": "Bw"}]
let key = initJwk(node)
var rsa = key.rsaPrv
rsa.n[0] = '!'
rsa.p.get[0] = '!'
rsa.oth.get[0].r[0] = '!'
check key.rsaPrv.n == node["n"].getStr
check key.rsaPrv.p.get == node["p"].getStr
check key.rsaPrv.oth.get[0].r == "Aw"
+47
View File
@@ -0,0 +1,47 @@
when compileOption("threads"):
import std/[json, options, tables, unittest]
import jwt_full
type
KeyCache = Table[string, JwkSet]
WorkerInput = tuple[keys: KeyCache, passed: ptr bool]
proc useKeys(input: WorkerInput) {.thread.} =
var keys = input.keys
try:
for i in 0 ..< 50:
let key = keys["issuer"][0]
let metadata = key["metadata"].get
metadata["labels"][0].str = "worker"
var material = key.octKey
material.k[0] = '!'
# Both metadata and parsed key material remain intact after modifying
# accessor results. Each worker also signs and verifies with its copy.
if key["metadata"].get["labels"][0].getStr != "original": return
if key.octKey.k[0] == '!': return
let token = sign(payload = "payload",
header = initJoseHeader(%*{"alg": "HS256"}), key = key)
token.validate(HS256, key)
keys["issuer"].setLen(0)
input.passed[] = true
except CatchableError:
input.passed[] = false
suite "jwt_full/jwk thread isolation":
test "workers copy key caches and use their keys independently":
let node = %*{
"kty": "oct", "alg": "HS256", "kid": "original",
"k": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY",
"metadata": {"labels": ["original"]}}
let original = {"issuer": @[initJwk(node)]}.toTable
var workers: array[4, Thread[WorkerInput]]
var passed: array[4, bool]
for i in 0 ..< workers.len:
createThread(workers[i], useKeys, (original, addr passed[i]))
joinThreads(workers)
check passed == [true, true, true, true]
check original["issuer"].len == 1
check original["issuer"][0]["metadata"].get == node["metadata"]
check original["issuer"][0].octKey.k == node["k"].getStr