Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d037c84e8c | ||
|
|
47393ec84f | ||
|
|
0f3b9bfe9a | ||
|
|
ac61d90585 |
No files matched your search
@@ -0,0 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## 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.
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
[RFC7518 (JWA)][rfc7518] registers
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# Package
|
||||
|
||||
version = "0.1.0"
|
||||
version = "0.5.0"
|
||||
author = "Jonathan Bernard"
|
||||
description = "Full JWT, JWS, JWE, and JWK implementation for Nim, compliant with RFCs 7515-7519."
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -13,4 +13,5 @@ requires "bearssl"
|
||||
requires "https://github.com/yglukhov/bearssl_pkey_decoder"
|
||||
|
||||
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"
|
||||
@@ -25,25 +25,29 @@ type
|
||||
JwtClaims* = object
|
||||
rawB64: string
|
||||
json: JsonNode
|
||||
iss: Option[string]
|
||||
sub: Option[string]
|
||||
aud: Option[string]
|
||||
exp: Option[DateTime]
|
||||
nbf: Option[DateTime]
|
||||
iat: Option[DateTime]
|
||||
iss: Option[string]
|
||||
jti: Option[string]
|
||||
nbf: Option[DateTime]
|
||||
sid: Option[string]
|
||||
sub: Option[string]
|
||||
typ: Option[string]
|
||||
|
||||
# Public read-only accessors to JwtClaims members
|
||||
# -----------------------------------------------
|
||||
|
||||
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 exp*(c: JwtClaims): Option[DateTime] = c.exp
|
||||
func nbf*(c: JwtClaims): Option[DateTime] = c.nbf
|
||||
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 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] =
|
||||
## Generic accessor to claim values by name
|
||||
@@ -61,13 +65,15 @@ proc initJwtClaims*(n: JsonNode): JwtClaims =
|
||||
return JwtClaims(
|
||||
rawB64: b64UrlEncode($n),
|
||||
json: n,
|
||||
iss: n.optStrVal("iss"),
|
||||
sub: n.optStrVal("sub"),
|
||||
aud: n.optStrVal("aud"),
|
||||
exp: n.optNumericDate("exp"),
|
||||
nbf: n.optNumericDate("nbf"),
|
||||
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 =
|
||||
## Parse a Base64url-encoded set of claims into a JwtClaims object
|
||||
@@ -77,13 +83,15 @@ proc initJwtClaims*(encoded: string): JwtClaims =
|
||||
result.rawB64 = encoded
|
||||
|
||||
proc initJwtClaims*(
|
||||
iss: Option[string] = none[string](),
|
||||
sub: Option[string] = none[string](),
|
||||
aud: Option[string] = none[string](),
|
||||
exp: Option[DateTime] = none[DateTime](),
|
||||
nbf: Option[DateTime] = none[DateTime](),
|
||||
iat: Option[DateTime] = none[DateTime](),
|
||||
iss: 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]] = @[]
|
||||
): JwtClaims =
|
||||
## Convenience method to initialize a new JwtClaims object by specifying
|
||||
@@ -99,12 +107,14 @@ proc initJwtClaims*(
|
||||
let json = newJObject()
|
||||
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 exp.isSome: json["exp"] = %toNumericDate(exp.get)
|
||||
if nbf.isSome: json["nbf"] = %toNumericDate(nbf.get)
|
||||
if iat.isSome: json["iat"] = %toNumericDate(iat.get)
|
||||
if iss.isSome: json["iss"] = %iss.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)
|
||||
@@ -11,7 +11,7 @@
|
||||
##
|
||||
## 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 ./private/jsonutils
|
||||
@@ -56,7 +56,12 @@ type
|
||||
k*: string
|
||||
|
||||
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
|
||||
use: 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 `[]`*(jwk: JWK, key: string): Option[JsonNode] =
|
||||
if jwk.json.hasKey(key): some(jwk.json[key])
|
||||
else: none[JsonNode]()
|
||||
## Return an independent JSON value for a member, or none if it is absent.
|
||||
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
|
||||
# ------------------------
|
||||
@@ -143,7 +158,9 @@ func parseOctetKey*(n: JsonNode): OctetKey =
|
||||
OctetKey(k: n.reqStrVal("k"))
|
||||
|
||||
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(""):
|
||||
|
||||
of $ktyEC:
|
||||
@@ -179,7 +196,8 @@ func initJwk*(n: JsonNode): JWK =
|
||||
else: raise newException(ValueError,
|
||||
"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.key_ops = n.optStrVal("key_ops")
|
||||
result.alg = n.optStrVal("alg")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# jwt_full/jwt.nim
|
||||
# Copyright 2021 Jonathan Bernard
|
||||
# Copyright © 2021 Jonathan Bernard
|
||||
|
||||
## ===============================
|
||||
## jwt
|
||||
@@ -57,7 +57,7 @@ proc initJWT*(encoded: string): JWT =
|
||||
jws: jws)
|
||||
else:
|
||||
# TODO
|
||||
raise newException(Exception, "not yet implemented")
|
||||
raise newException(Exception, "partial JWT construction is not yet implemented")
|
||||
|
||||
proc toJWT*(encoded: string): JWT = initJWT(encoded)
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import std/tables
|
||||
import bearssl, bearssl_pkey_decoder
|
||||
|
||||
import ../../encoding
|
||||
@@ -35,14 +34,14 @@ func fromBearSslCurveConst(curve: int32): EcCurve =
|
||||
func initEcPublicKeyObj(curve: EcCurve, q: string): EcPublicKeyObj =
|
||||
result = EcPublicKeyObj(curve: curve, q: q)
|
||||
result.bearKey.curve = curve.toBearSslCurveConst
|
||||
result.bearKey.q = cast[ptr char](result.q.cstring)
|
||||
result.bearKey.qlen = q.len
|
||||
result.bearKey.q = cast[ptr byte](result.q.cstring)
|
||||
result.bearKey.qlen = uint q.len
|
||||
|
||||
func initEcPrivateKeyObj(curve: EcCurve, x: string): EcPrivateKeyObj =
|
||||
result = EcPrivateKeyObj(curve: curve, x: x)
|
||||
result.bearKey.curve = curve.toBearSslCurveConst
|
||||
result.bearKey.x = cast[ptr char](result.x.cstring)
|
||||
result.bearKey.xlen = x.len
|
||||
result.bearKey.x = cast[ptr byte](result.x.cstring)
|
||||
result.bearKey.xlen = uint x.len
|
||||
|
||||
proc toEcPublicKey(jwk: JWK): EcPublicKeyObj =
|
||||
## 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))
|
||||
|
||||
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 =
|
||||
## 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 =
|
||||
var skCtx: SkeyDecoderContext
|
||||
skeyDecoderInit(addr skCtx)
|
||||
skeyDecoderInit(skCtx)
|
||||
decodePem(pem, "EC PRIVATE KEY", addr skCtx,
|
||||
cast[BearPemDecoderCallback](skeyDecoderPush))
|
||||
|
||||
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 =
|
||||
let hashAlg = case alg:
|
||||
@@ -137,11 +136,11 @@ proc bearEcVerify(
|
||||
let ecVerifyImpl = ecdsaVrfyRawGetDefault()
|
||||
let resultCode = ecVerifyImpl(
|
||||
addr ecAllM15,
|
||||
cast[ptr char](unsafeAddr hashed[0]),
|
||||
hashed.len,
|
||||
hashed.cstring,
|
||||
uint hashed.len,
|
||||
unsafeAddr key.bearKey,
|
||||
cast[ptr char](unsafeAddr signature[0]),
|
||||
signature.len)
|
||||
signature.cstring,
|
||||
uint signature.len)
|
||||
|
||||
result = resultCode == 1
|
||||
|
||||
|
||||
@@ -66,5 +66,5 @@ proc hash*(data: string, alg: HashAlgorithm): string =
|
||||
let hashCfg = hashConfig(alg)
|
||||
result = newString(hashCfg.size)
|
||||
hashCfg.vtable.init(pCtx)
|
||||
hashCfg.vtable.update(pCtx, unsafeAddr data[0], data.len)
|
||||
hashCfg.vtable.output(pCtx, addr result[0])
|
||||
hashCfg.vtable.update(pCtx, data.cstring, uint data.len)
|
||||
hashCfg.vtable.out(pCtx, addr result[0])
|
||||
@@ -26,13 +26,13 @@ proc bearHMAC(message: string, alg: JwtAlgorithm, key: string): string =
|
||||
var keyCtx: HmacKeyContext
|
||||
var hmacCtx: HmacContext
|
||||
|
||||
hmacKeyInit(addr keyCtx, vtable, key.cstring, key.len)
|
||||
hmacInit(addr hmacCtx, addr keyCtx, 0)
|
||||
hmacUpdate(addr hmacCtx, message.cstring, message.len)
|
||||
hmacKeyInit(keyCtx, vtable, key.cstring, uint key.len)
|
||||
hmacInit(hmacCtx, keyCtx, uint 0)
|
||||
hmacUpdate(hmacCtx, message.cstring, uint message.len)
|
||||
|
||||
let resLen = hmacSize(addr hmacCtx)
|
||||
let resLen = hmacSize(hmacCtx)
|
||||
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 =
|
||||
return bearHMAC(message, alg, key)
|
||||
|
||||
@@ -3,7 +3,7 @@ import bearssl
|
||||
# Taken from nim-bearssl/decls.nim
|
||||
{.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*(
|
||||
pem: string,
|
||||
@@ -13,30 +13,30 @@ proc decodePem*(
|
||||
) =
|
||||
|
||||
var pemCtx: PemDecoderContext
|
||||
pemDecoderInit(addr pemCtx)
|
||||
pemDecoderInit(pemCtx)
|
||||
|
||||
var offset = 0
|
||||
var bytesRemaining = len(pem)
|
||||
var offset: uint = 0
|
||||
var bytesRemaining: uint = uint len(pem)
|
||||
var readingObj = false
|
||||
|
||||
while bytesRemaining > 0:
|
||||
let bytesRead = pemDecoderPush(addr pemCtx, unsafeAddr pem[offset], bytesRemaining)
|
||||
let bytesRead = pemDecoderPush(pemCtx, unsafeAddr pem[offset], uint bytesRemaining)
|
||||
offset += bytesRead
|
||||
bytesRemaining -= bytesRead
|
||||
|
||||
case pemDecoderEvent(addr pemCtx):
|
||||
case pemDecoderEvent(pemCtx):
|
||||
of PEM_BEGIN_OBJ:
|
||||
if readingObj:
|
||||
raise newException(ValueError,
|
||||
"Invalid PEM: saw a second BEGIN before seeing END.")
|
||||
|
||||
if pemDecoderName(addr pemCtx) != expectedObjectName:
|
||||
if pemDecoderName(pemCtx) != expectedObjectName:
|
||||
raise newException(ValueError,
|
||||
"Invalid PEM: expected BEGIN " & expectedObjectName &
|
||||
" but got BEGIN " & $pemDecoderName(addr pemCtx))
|
||||
" but got BEGIN " & $pemDecoderName(pemCtx))
|
||||
|
||||
readingObj = true
|
||||
pemDecoderSetdest(addr pemCtx, callback, keyCtx)
|
||||
pemDecoderSetdest(pemCtx, callback, keyCtx)
|
||||
|
||||
of PEM_END_OBJ:
|
||||
if readingObj: readingObj = false
|
||||
|
||||
@@ -19,24 +19,24 @@ type
|
||||
|
||||
func initRsaPublicKeyObj(n, e: string): RsaPublicKeyObj =
|
||||
result = RsaPublicKeyObj(n: n, e: e)
|
||||
result.bearKey.n = cast[ptr char](result.n.cstring)
|
||||
result.bearKey.nlen = result.n.len
|
||||
result.bearKey.e = cast[ptr char](result.e.cstring)
|
||||
result.bearKey.elen = result.e.len
|
||||
result.bearKey.n = cast[ptr byte](result.n.cstring)
|
||||
result.bearKey.nlen = uint result.n.len
|
||||
result.bearKey.e = cast[ptr byte](result.e.cstring)
|
||||
result.bearKey.elen = uint result.e.len
|
||||
|
||||
func initRsaPrivateKeyObj(nBitLen: int, p, q, dp, dq, iq: string): RsaPrivateKeyObj =
|
||||
result = RsaPrivateKeyObj(p: p, q: q, dp: dp, dq: dq, iq: iq)
|
||||
result.bearKey.nBitLen = cast[uint32](nBitLen)
|
||||
result.bearKey.p = cast[ptr char](result.p.cstring)
|
||||
result.bearKey.plen = result.p.len
|
||||
result.bearKey.q = cast[ptr char](result.q.cstring)
|
||||
result.bearKey.qlen = result.q.len
|
||||
result.bearKey.dp = cast[ptr char](result.dp.cstring)
|
||||
result.bearKey.dplen = result.dp.len
|
||||
result.bearKey.dq = cast[ptr char](result.dq.cstring)
|
||||
result.bearKey.dqlen = result.dq.len
|
||||
result.bearKey.iq = cast[ptr char](result.iq.cstring)
|
||||
result.bearKey.iqlen = result.iq.len
|
||||
result.bearKey.p = cast[ptr byte](result.p.cstring)
|
||||
result.bearKey.plen = uint result.p.len
|
||||
result.bearKey.q = cast[ptr byte](result.q.cstring)
|
||||
result.bearKey.qlen = uint result.q.len
|
||||
result.bearKey.dp = cast[ptr byte](result.dp.cstring)
|
||||
result.bearKey.dplen = uint result.dp.len
|
||||
result.bearKey.dq = cast[ptr byte](result.dq.cstring)
|
||||
result.bearKey.dqlen = uint result.dq.len
|
||||
result.bearKey.iq = cast[ptr byte](result.iq.cstring)
|
||||
result.bearKey.iqlen = uint result.iq.len
|
||||
|
||||
proc toRsaPublicKey(jwk: JWK): RsaPublicKeyObj =
|
||||
## 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))
|
||||
|
||||
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 =
|
||||
@@ -111,7 +111,7 @@ proc toRsaPrivateKey(jwk: JWK): RsaPrivateKeyObj =
|
||||
|
||||
proc toRsaPrivateKey(pem: string): RsaPrivateKeyObj =
|
||||
var skCtx: SkeyDecoderContext
|
||||
skeyDecoderInit(addr skCtx)
|
||||
skeyDecoderInit(skCtx)
|
||||
decodePem(pem, "RSA PRIVATE KEY", addr skCtx,
|
||||
cast[BearPemDecoderCallback](skeyDecoderPush))
|
||||
|
||||
@@ -119,11 +119,11 @@ proc toRsaPrivateKey(pem: string): RsaPrivateKeyObj =
|
||||
|
||||
return initRsaPrivateKeyObj(
|
||||
cast[int](k.nBitLen),
|
||||
$cstring(k.p),
|
||||
$cstring(k.q),
|
||||
$cstring(k.dp),
|
||||
$cstring(k.dq),
|
||||
$cstring(k.iq))
|
||||
$cast[cstring](k.p),
|
||||
$cast[cstring](k.q),
|
||||
$cast[cstring](k.dp),
|
||||
$cast[cstring](k.dq),
|
||||
$cast[cstring](k.iq))
|
||||
|
||||
proc getRsaHashCfg(alg: JwtAlgorithm): HashCfg =
|
||||
let hashAlg = case alg:
|
||||
@@ -148,11 +148,11 @@ proc bearRsaSign(
|
||||
result = newString((key.bearKey.nBitLen + 7) div 8)
|
||||
|
||||
let errCode = rsaSignImpl(
|
||||
cast[ptr char](hashCfg.oid),
|
||||
cast[ptr char](unsafeAddr hashed[0]),
|
||||
hashed.len,
|
||||
cast[ptr byte](hashCfg.oid),
|
||||
cast[ptr byte](unsafeAddr hashed[0]),
|
||||
uint hashed.len,
|
||||
unsafeAddr key.bearKey,
|
||||
cast[ptr char](addr result[0]))
|
||||
cast[ptr byte](addr result[0]))
|
||||
|
||||
if errCode != 1: raise newException(Exception, "RSA signature failed")
|
||||
|
||||
@@ -169,12 +169,12 @@ proc bearRsaVerify(
|
||||
var recoveredHash = newString(hashCfg.size)
|
||||
|
||||
let errCode = rsaVerifyImpl(
|
||||
cast[ptr char](unsafeAddr signature[0]),
|
||||
signature.len,
|
||||
cast[ptr char](hashCfg.oid),
|
||||
hashed.len,
|
||||
cast[ptr byte](unsafeAddr signature[0]),
|
||||
uint signature.len,
|
||||
cast[ptr byte](hashCfg.oid),
|
||||
uint hashed.len,
|
||||
unsafeAddr key.bearKey,
|
||||
cast[ptr char](addr recoveredHash[0]))
|
||||
cast[ptr byte](addr recoveredHash[0]))
|
||||
|
||||
if errCode != 1: return false
|
||||
return hashed == recoveredHash
|
||||
|
||||
@@ -4,5 +4,6 @@ import ./tclaims
|
||||
import ./tencoding
|
||||
import ./tjoseheader
|
||||
import ./tjwk
|
||||
import ./tjwkthreads
|
||||
import ./tjws
|
||||
import ./tjwt
|
||||
@@ -2,6 +2,7 @@ import std/json, std/options, std/unittest
|
||||
|
||||
import jwt_full/jwa
|
||||
import jwt_full/joseheader
|
||||
import jwt_full/jwk
|
||||
|
||||
import ../testdata
|
||||
|
||||
@@ -16,3 +17,16 @@ suite "jwt_full/joseheader":
|
||||
h1.rawB64 == rfc7519_S31_HeaderB64
|
||||
h2.rawB64 == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"
|
||||
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
@@ -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
|
||||
|
||||
@@ -215,3 +215,88 @@ suite "jwt_full/jwk":
|
||||
jwkSet[0].keyKind == EcPrivate
|
||||
jwkSet[1].kty == $ktyRSA
|
||||
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"
|
||||
@@ -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
|
||||
Reference in new issue
Block a user