Rename to jwt_full to avoid collision with existing nim-jwt.
This commit is contained in:
1 parent
35a9cf682c
commit
4cc2a4dee5
25 files changed
+30
-30
No files matched your search
@@ -0,0 +1,109 @@
|
||||
# jwt_full/claims.nim
|
||||
# Copyright 2021 Jonathan Bernard
|
||||
|
||||
## ====================================
|
||||
## claims
|
||||
## ====================================
|
||||
##
|
||||
## ------------------------------------
|
||||
## JWT Claims Object
|
||||
## ------------------------------------
|
||||
##
|
||||
## Data structures and utility procedures for working with JWT claims objects.
|
||||
## This module follows the common pattern in this library of providing direct
|
||||
## access to all of the claims members via the `[]` syntax, as well as
|
||||
## convencience accessors for the publicly-defined claims.
|
||||
|
||||
import std/json, std/options, std/times
|
||||
|
||||
import ./private/encoding
|
||||
import ./private/jsonutils
|
||||
import ./private/timeutils
|
||||
|
||||
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]
|
||||
jti: 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 jti*(c: JwtClaims): Option[string] = c.jti
|
||||
|
||||
func `[]`*(c: JwtClaims, key: string): Option[JsonNode] =
|
||||
## Generic accessor to claim values by name
|
||||
if c.json.hasKey(key): some(c.json[key])
|
||||
else: none[JsonNode]()
|
||||
|
||||
proc `$`*(claims: JwtClaims): string =
|
||||
## Serialize a JwtClaims object to a Base64url-encoded string (as per the JWT
|
||||
## standard).
|
||||
claims.rawB64
|
||||
|
||||
proc initJwtClaims*(n: JsonNode): JwtClaims =
|
||||
## Create a JwtClaims from a given set of claims as a JsonNOde
|
||||
|
||||
return JwtClaims(
|
||||
rawB64: $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"))
|
||||
|
||||
proc initJwtClaims*(encoded: string): JwtClaims =
|
||||
## Parse a Base64url-encoded set of claims into a JwtClaims object
|
||||
|
||||
let decoded = b64UrlDecode(encoded)
|
||||
result = initJwtClaims(parseJson(decoded))
|
||||
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](),
|
||||
jti: Option[string] = none[string](),
|
||||
moreClaims: seq[tuple[k, v: string]] = @[]
|
||||
): JwtClaims =
|
||||
## Convenience method to initialize a new JwtClaims object by specifying
|
||||
## fields directly.
|
||||
runnableExamples:
|
||||
let claims = initJwtClaims(
|
||||
iss = "https://issuer-id",
|
||||
sub = "jqt",
|
||||
iat = now(),
|
||||
exp = now() + 10.minutes,
|
||||
moreClaims: @[("name", "John Q. Tester")])
|
||||
|
||||
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 jti.isSome: json["jti"] = %jti.get
|
||||
|
||||
return initJwtClaims(json)
|
||||
@@ -0,0 +1,126 @@
|
||||
# jwt_full/joseheader.nim
|
||||
# Copyright 2021 Jonathan Bernard
|
||||
|
||||
## ====================================
|
||||
## joseheader
|
||||
## ====================================
|
||||
##
|
||||
## ------------------------------------
|
||||
## JOSE Header
|
||||
## ------------------------------------
|
||||
##
|
||||
## Data structures and utility procedures for working with JOSE headers. This
|
||||
## module follows the common pattern in this library of providing direct access
|
||||
## to all of the header parameters via the `[]` syntax, as well as convencience
|
||||
## accessors for the publicly-defined header parameters.
|
||||
|
||||
import std/json, std/options, std/sequtils, std/strutils
|
||||
|
||||
import ./jwa, ./jwk
|
||||
import ./private/encoding
|
||||
import ./private/jsonutils
|
||||
|
||||
type
|
||||
JoseHeader* = object
|
||||
rawB64: string
|
||||
json: JsonNode
|
||||
alg: Option[JwtAlgorithm]
|
||||
jku: Option[string]
|
||||
jwk: Option[JWK]
|
||||
kid: Option[string]
|
||||
typ: Option[string]
|
||||
cty: Option[string]
|
||||
crit: Option[string]
|
||||
x5u: Option[string]
|
||||
x5c: Option[seq[string]]
|
||||
x5t: Option[string]
|
||||
x5tS256: Option[string]
|
||||
|
||||
func rawB64*(h: JoseHeader): string = h.rawB64
|
||||
func alg*(h: JoseHeader): Option[JwtAlgorithm] = h.alg
|
||||
func jku*(h: JoseHeader): Option[string] = h.jku
|
||||
func jwk*(h: JoseHeader): Option[JWK] = h.jwk
|
||||
func kid*(h: JoseHeader): Option[string] = h.kid
|
||||
func typ*(h: JoseHeader): Option[string] = h.typ
|
||||
func cty*(h: JoseHeader): Option[string] = h.crit
|
||||
func crit*(h: JoseHeader): Option[string] = h.crit
|
||||
func x5u*(h: JoseHeader): Option[string] = h.x5u
|
||||
func x5c*(h: JoseHeader): Option[seq[string]] = h.x5c
|
||||
func x5t*(h: JoseHeader): Option[string] = h.x5t
|
||||
func x5tS256*(h: JoseHeader): Option[string] = h.x5tS256
|
||||
|
||||
func `[]`*(h: JoseHeader, key: string): Option[JsonNode] =
|
||||
if h.json.hasKey(key): some(h.json[key])
|
||||
else: none[JsonNode]()
|
||||
|
||||
proc `$`*(header: JoseHeader): string = header.rawB64
|
||||
|
||||
proc initJoseHeader*(n: JsonNode): JoseHeader =
|
||||
return JoseHeader(
|
||||
rawB64: b64UrlEncode($n),
|
||||
json: n,
|
||||
alg: if n.hasKey("alg"): some(parseEnum[JwtAlgorithm](n["alg"].getStr))
|
||||
else: none[JwtAlgorithm](),
|
||||
jku: n.optStrVal("jku"),
|
||||
jwk: if n.hasKey("jwk"): some(initJwk(n["jwk"]))
|
||||
else: none[JWK](),
|
||||
kid: n.optStrVal("kid"),
|
||||
typ: n.optStrVal("typ"),
|
||||
cty: n.optStrVal("cty"),
|
||||
crit: n.optStrVal("crit"),
|
||||
x5u: n.optStrVal("x5u"),
|
||||
x5c: if n.hasKey("x5c"): some(n["x5c"].getElems.mapIt(it.getStr))
|
||||
else: none[seq[string]](),
|
||||
x5t: n.optStrVal("x5t"),
|
||||
x5tS256: n.optStrVal("x5tS256"))
|
||||
|
||||
proc initJoseHeader*(encoded: string): JoseHeader =
|
||||
let decoded = b64UrlDecode(encoded)
|
||||
result = initJoseHeader(parseJson(decoded))
|
||||
result.rawB64 = encoded
|
||||
|
||||
proc initJoseHeader*(
|
||||
alg: Option[JwtAlgorithm] = some(HS256),
|
||||
jku: Option[string] = none[string](),
|
||||
jwk: Option[JWK] = none[JWK](),
|
||||
kid: Option[string] = none[string](),
|
||||
typ: Option[string] = none[string](),
|
||||
cty: Option[string] = none[string](),
|
||||
crit: Option[string] = none[string](),
|
||||
x5u: Option[string] = none[string](),
|
||||
x5c: Option[seq[string]] = none[seq[string]](),
|
||||
x5t: Option[string] = none[string](),
|
||||
x5tS256: Option[string] = none[string](),
|
||||
moreParams: seq[tuple[k, v: string]] = @[]
|
||||
): JoseHeader =
|
||||
|
||||
let json = newJObject()
|
||||
for p in moreParams: json[p.k] = %p.v
|
||||
|
||||
if alg.isSome: json["alg"] = %alg.get
|
||||
if jku.isSome: json["jku"] = %jku.get
|
||||
if jwk.isSome: json["jwk"] = %jwk.get
|
||||
if kid.isSome: json["kid"] = %kid.get
|
||||
if typ.isSome: json["typ"] = %typ.get
|
||||
if cty.isSome: json["cty"] = %cty.get
|
||||
if crit.isSome: json["crit"] = %crit.get
|
||||
if x5u.isSome: json["x5u"] = %x5u.get
|
||||
if x5c.isSome: json["x5c"] = %x5c.get
|
||||
if x5t.isSome: json["x5t"] = %x5t.get
|
||||
if x5tS256.isSome: json["x5tS256"] = %x5tS256.get
|
||||
|
||||
return initJoseHeader(json)
|
||||
|
||||
proc combine*(a, b: JoseHeader): JoseHeader =
|
||||
let json = newJObject()
|
||||
|
||||
for k, v in a.json: json[k] = v
|
||||
|
||||
for k, v in b.json:
|
||||
if json.hasKey(k):
|
||||
raise newException(ValueError,
|
||||
"Duplicate key '" & k & "' found when combining JoseHeaders. This " &
|
||||
"will result in an invalid JWS, JWE, or JWT.")
|
||||
json[k] = v
|
||||
|
||||
return initJoseHeader(json)
|
||||
@@ -0,0 +1,45 @@
|
||||
# jwt_full/jwa.nim
|
||||
# Copyright 2021 Jonathan Bernard
|
||||
|
||||
##
|
||||
## ====================================
|
||||
## jwa
|
||||
## ====================================
|
||||
##
|
||||
## ------------------------------------
|
||||
## JSON Web Algorithms (JWA) - RFC 7518
|
||||
## ------------------------------------
|
||||
##
|
||||
## Enum values reflecting the names of algorithms defined by JWA RFC 7518.
|
||||
|
||||
type
|
||||
JwtKeyType* = enum ktyEC = "EC", ktyRSA = "RSA", ktyOctet = "oct"
|
||||
|
||||
JwtAlgorithm* = 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"
|
||||
|
||||
EcCurve* = enum P256 = "P-256", P384 = "P-384", P521 = "P-521"
|
||||
@@ -0,0 +1,19 @@
|
||||
# jwt_full/jwe.nim
|
||||
# Copyright 2021 Jonathan Bernard
|
||||
|
||||
## ====================================
|
||||
## jwe
|
||||
## ====================================
|
||||
##
|
||||
## ------------------------------------
|
||||
## JSON Web Encryption (JWE) - RFC 7516
|
||||
## ------------------------------------
|
||||
##
|
||||
|
||||
import ./joseheader
|
||||
|
||||
type
|
||||
JWE* = object
|
||||
header: JoseHeader
|
||||
|
||||
func header*(jwe: JWE): JoseHeader = jwe.header
|
||||
@@ -0,0 +1,198 @@
|
||||
# jwt_full/jwk.nim
|
||||
# Copyright 2021 Jonathan Bernard
|
||||
|
||||
## ==============================
|
||||
## jwk
|
||||
## ==============================
|
||||
##
|
||||
## ------------------------------
|
||||
## JSON Web Keys (JWK) - RFC 7518
|
||||
## ------------------------------
|
||||
##
|
||||
## Nim implementation of the data structures and constants defined in RFC 7518.
|
||||
|
||||
import std/json, std/options, std/sequtils, std/strutils
|
||||
|
||||
import ./jwa
|
||||
import ./private/jsonutils
|
||||
|
||||
type
|
||||
JwkKeyType* = enum EcPublic, EcPrivate, RsaPublic, RsaPrivate, Octet
|
||||
|
||||
JwkUse* = enum jwkuSignature = "sig", jwkuEncrypt = "enc"
|
||||
|
||||
JwkOps* = enum jwkopSign = "sign", jwkopVerify = "verify",
|
||||
jwkopEncrypt = "encrypt", jwkopDecrypt = "decrypt",
|
||||
jwkopWrapKey = "wrapKey", jwkopUnwrapKey = "unwrapKey",
|
||||
jwkopDeriveKey = "deriveKey", jwkopDeriveBits = "deriveBits"
|
||||
|
||||
EcPubKey = object of RootObj
|
||||
crv*: EcCurve
|
||||
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]
|
||||
|
||||
# Read-only public accessors to JWK members
|
||||
# -----------------------------------------
|
||||
|
||||
func kty*(jwk: JWK): string = jwk.kty
|
||||
func use*(jwk: JWK): Option[string] = jwk.use
|
||||
func key_ops*(jwk: JWK): Option[string] = jwk.key_ops
|
||||
func alg*(jwk: JWK): Option[string] = jwk.alg
|
||||
func kid*(jwk: JWK): Option[string] = jwk.kid
|
||||
func x5u*(jwk: JWK): Option[string] = jwk.x5u
|
||||
func x5c*(jwk: JWK): Option[string] = jwk.x5c
|
||||
func x5t*(jwk: JWK): Option[string] = jwk.x5t
|
||||
func x5tS256*(jwk: JWK): Option[string] = jwk.x5tS256
|
||||
func keyKind*(jwk: JWK): JwkKeyType = jwk.keyKind
|
||||
func ecPub*(jwk: JWK): EcPubKey = jwk.ecPub
|
||||
func ecPrv*(jwk: JWK): EcPrvKey = jwk.ecPrv
|
||||
func rsaPub*(jwk: JWK): RsaPubKey = jwk.rsaPub
|
||||
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]()
|
||||
|
||||
# Public Parsing Functions
|
||||
# ------------------------
|
||||
|
||||
func parseEcPubKey*(n: JsonNode): EcPubKey =
|
||||
EcPubKey(
|
||||
crv: parseEnum[EcCurve](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 initJwk*(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 initJwkSet*(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(initJwk(it))
|
||||
@@ -0,0 +1,253 @@
|
||||
# jwt_full/jws.nim
|
||||
# Copyright 2021 Jonathan Bernard
|
||||
|
||||
## ====================================
|
||||
## jws
|
||||
## ====================================
|
||||
##
|
||||
## ------------------------------------
|
||||
## JSON Web Signature (JWS) - RFC 7515
|
||||
## ------------------------------------
|
||||
##
|
||||
|
||||
import std/json, std/logging, std/options, std/sequtils, std/strutils
|
||||
|
||||
import ./private/crypto
|
||||
import ./private/encoding
|
||||
import ./private/jsonutils
|
||||
|
||||
import ./claims
|
||||
import ./joseheader
|
||||
import ./jwa
|
||||
import ./jwk
|
||||
import ./jwssig
|
||||
|
||||
const VALID_SIGNATURE_ALGORITHMS = [
|
||||
HS256, HS384, HS512, RS256, RS384, RS512,
|
||||
ES256, ES384, ES512, PS256, PS384, PS512
|
||||
]
|
||||
|
||||
type
|
||||
InvalidSignature* = object of CatchableError
|
||||
|
||||
JWS* = object
|
||||
payloadB64: string
|
||||
signatures: seq[JwsSignature]
|
||||
compactSerialization: Option[string]
|
||||
|
||||
func payloadB64*(jws: JWS): string = jws.payloadB64
|
||||
func signatures*(jws: JWS): seq[JwsSignature] = jws.signatures
|
||||
|
||||
func `[]`*(jws: JWS, idx: int): JwsSignature = jws.signatures[idx]
|
||||
func len*(jws: JWS): int = jws.signatures.len
|
||||
|
||||
func compactSerialization*(jws: JWS): Option[string] =
|
||||
jws.compactSerialization
|
||||
|
||||
func jsonSerialization*(jws: JWS, flatten = false): string =
|
||||
if flatten and jws.len != 1:
|
||||
raise newException(ValueError, "A JWS must have exactly one signature " &
|
||||
"in order to be serialized in the Flattened JSON Serialization " &
|
||||
"Syntax. This JWS has " & $(jws.len) & " signatures.")
|
||||
|
||||
elif flatten:
|
||||
return $(%*{
|
||||
"payload": jws.payloadB64,
|
||||
"protected": jws[0].protected.rawB64,
|
||||
"header": jws[0].header.rawB64,
|
||||
"signature": jws[0].signatureB64
|
||||
})
|
||||
|
||||
else:
|
||||
return $(%*{
|
||||
"payload": jws.payloadB64,
|
||||
"signatures": jws.signatures
|
||||
})
|
||||
|
||||
func `$`*(jws: JWS): string =
|
||||
if jws.len == 1: return jws.compactSerialization.get
|
||||
else: return jws.jsonSerialization
|
||||
|
||||
proc validateHeader(h: JoseHeader) =
|
||||
if not h.alg.isSome:
|
||||
raise newException(ValueError, "missing required alg header parameter")
|
||||
|
||||
if not VALID_SIGNATURE_ALGORITHMS.contains(h.alg.get):
|
||||
raise newException(ValueError,
|
||||
$(h.alg) & " is not a valid signature algorithm.")
|
||||
|
||||
proc initJWS*(b64: string): JWS =
|
||||
let parts = b64.split('.')
|
||||
|
||||
if parts.len != 3:
|
||||
raise newException(ValueError,
|
||||
"invalid JWS, expected three parts but only found " & $parts.len)
|
||||
|
||||
debug "Base64url-encoded payload is:\n\t" & parts[1]
|
||||
|
||||
result = JWS(
|
||||
payloadB64: parts[1],
|
||||
signatures: @[ initJwsSignature(
|
||||
protected = initJoseHeader(parts[0]),
|
||||
header = initJoseHeader(alg = none[JwtAlgorithm]()),
|
||||
signatureB64 = parts[2]) ])
|
||||
result.compactSerialization = some(b64)
|
||||
|
||||
proc initJWS*(n: JsonNode): JWS =
|
||||
if not n.hasKey("payload"):
|
||||
raise newException(ValueError, "invalid JWS: missing 'payload'")
|
||||
|
||||
if n.hasKey("signatures"):
|
||||
result = JWS(
|
||||
payloadB64: n.reqStrVal("payload"),
|
||||
signatures: n["signatures"].getElems.mapIt(initJwsSignature(it)))
|
||||
if result.len == 0:
|
||||
raise newException(ValueError, "Invalid JWS: no signatures.")
|
||||
else:
|
||||
result = JWS(
|
||||
payloadB64: n.reqStrVal("payload"),
|
||||
signatures: @[initJwsSignature(n)])
|
||||
|
||||
proc sign*(
|
||||
payload: string,
|
||||
header: JoseHeader,
|
||||
key: JWK | string,
|
||||
payloadIsB64Encoded = false
|
||||
): JWS =
|
||||
## Create a JWS signing the given payload, which can be any arbitrary data.
|
||||
## The returned JWS can be serialized with the compact serialization. All
|
||||
## header data will be protected.
|
||||
|
||||
validateHeader(header)
|
||||
|
||||
let payloadB64 = if payloadIsB64Encoded: payload
|
||||
else: b64UrlEncode(payload)
|
||||
|
||||
let valueToSign = $header & "." & payloadB64
|
||||
let signatureB64 =
|
||||
b64UrlEncode(computeSignature(valueToSign, header.alg.get, key))
|
||||
|
||||
debug "Signed a value:\n\ttoSign : " & valueToSign &
|
||||
"\n\tsignature: " & signatureB64
|
||||
|
||||
result = JWS(
|
||||
payloadB64: payloadB64,
|
||||
signatures: @[
|
||||
initJwsSignature(
|
||||
protected = header,
|
||||
signatureB64 = signatureB64)])
|
||||
|
||||
result.compactSerialization =
|
||||
some($header & "." & result.payloadB64 & "." & signatureB64)
|
||||
|
||||
proc sign*(claims: JwtClaims, header: JoseHeader, key: JWK): JWS =
|
||||
## Create a JWS signing a set of JWT claims. The returned JWS will be a valid
|
||||
## JWT.
|
||||
sign($claims, header, key, payloadIsB64Encoded = true)
|
||||
|
||||
proc sign*(
|
||||
payload: string,
|
||||
unprotected: JoseHeader,
|
||||
protected: JoseHeader,
|
||||
key: JWK | string,
|
||||
payloadIsB64Encoded = false): JWS =
|
||||
## Create a JWS signing the given payload, which can be arbitrary data. The
|
||||
## returned JWS can be serialized with the compact serialization.
|
||||
|
||||
let combinedHeader = combine(unprotected, protected)
|
||||
validateHeader(combinedHeader)
|
||||
|
||||
let payloadB64 = if payloadIsB64Encoded: payload
|
||||
else: b64UrlEncode(payload)
|
||||
|
||||
let valueToSign = $protected & "." & payloadB64
|
||||
let signatureB64 = b64UrlEncode(
|
||||
computeSignature(valueToSign, combinedHeader.alg.get, key))
|
||||
|
||||
debug "Signed a value:\n\ttoSign : " & valueToSign &
|
||||
"\n\tsignature: " & signatureB64
|
||||
|
||||
result = JWS(
|
||||
payloadB64: payloadB64,
|
||||
signatures: @[
|
||||
initJwsSignature(
|
||||
protected = protected,
|
||||
header = unprotected,
|
||||
signatureB64 = signatureB64)])
|
||||
|
||||
result.compactSerialization =
|
||||
some($combinedHeader & "." & result.payloadB64 & "." & signatureB64)
|
||||
|
||||
proc sign*(
|
||||
jws: JWS,
|
||||
unprotected: JoseHeader,
|
||||
protected: JoseHeader,
|
||||
key: JWK | string): JWS =
|
||||
## Create a new JWS signing the payload again with the newly supplied header
|
||||
## and key. The resulting JWS token will have multiple signatures and cannot
|
||||
## be serialized with the compact serialization.
|
||||
|
||||
let combinedHeader = combine(unprotected, protected)
|
||||
validateHeader(combinedHeader)
|
||||
|
||||
let valueToSign = $protected & "." & jws.payloadB64
|
||||
let signatureB64 = b64UrlEncode(
|
||||
computeSignature(valueToSign, combinedHeader.alg.get, key))
|
||||
|
||||
debug "Signed a value:\n\ttoSign : " & valueToSign &
|
||||
"\n\tsignature: " & signatureB64
|
||||
|
||||
result = JWS(
|
||||
payloadB64: jws.payloadB64,
|
||||
signatures: jws.signatures &
|
||||
@[initJwsSignature(
|
||||
protected = protected,
|
||||
header = unprotected,
|
||||
signatureB64 = signatureB64)])
|
||||
|
||||
result.compactSerialization = none[string]()
|
||||
|
||||
proc validate*(jws: JWS, alg: JwtAlgorithm, key: JWK | string, sigIdx = 0) =
|
||||
|
||||
if jws.len == 0:
|
||||
raise newException(InvalidSignature, "JWS has no signature.")
|
||||
|
||||
if jws.len < sigIdx+1:
|
||||
raise newException(InvalidSignature,
|
||||
"No signature at index " & $sigIdx & ". There are only " & $jws.len &
|
||||
" signatures on this JWS.")
|
||||
|
||||
let combinedHeader = combine(jws[sigIdx].header, jws[sigIdx].protected)
|
||||
|
||||
if combinedHeader.alg.isNone:
|
||||
raise newException(InvalidSignature, "JWS header has no value for 'alg'.")
|
||||
|
||||
if alg != combinedHeader.alg.get:
|
||||
raise newException(InvalidSignature,
|
||||
"JWS alg (" & $(combinedHeader.alg.get) & ") does not match the " &
|
||||
"requested alg (" & $alg & ")")
|
||||
|
||||
if not VALID_SIGNATURE_ALGORITHMS.contains(alg):
|
||||
raise newException(InvalidSignature,
|
||||
"'" & $alg & "' is not a valid signing algorithm.")
|
||||
|
||||
let payload = $(jws[sigIdx].protected) & "." & jws.payloadB64
|
||||
|
||||
debug "Verifying a JWS:\n\tpayload : " & payload &
|
||||
"\n\tsignature: " & jws[sigIdx].signatureB64
|
||||
|
||||
|
||||
if not verifySignature(
|
||||
payload = payload,
|
||||
signature = b64UrlDecode(jws[sigIdx].signatureB64),
|
||||
alg = alg,
|
||||
key = key):
|
||||
|
||||
raise newException(InvalidSignature, "failed to verify signature value.")
|
||||
|
||||
|
||||
proc tryValidate*(jws: JWS, alg: JwtAlgorithm, key: JWK | string): bool =
|
||||
try:
|
||||
jws.validate(alg, key)
|
||||
return true
|
||||
except: return false
|
||||
@@ -0,0 +1,55 @@
|
||||
# jwt_full/jwssig.nim
|
||||
# Copyright 2021 Jonathan Bernard
|
||||
|
||||
## =====================================
|
||||
## JwsSig
|
||||
## =====================================
|
||||
##
|
||||
## -------------------------------------
|
||||
## JwsSignature object and procedures.
|
||||
## -------------------------------------
|
||||
|
||||
import std/json, std/options
|
||||
|
||||
import ./jwa
|
||||
import ./joseheader
|
||||
import ./private/jsonutils
|
||||
|
||||
type
|
||||
JwsSignature* = object
|
||||
protected: JoseHeader
|
||||
header: JoseHeader
|
||||
signatureB64: string
|
||||
|
||||
func protected*(sig: JwsSignature): JoseHeader = sig.protected
|
||||
func header*(sig: JwsSignature): JoseHeader = sig.header
|
||||
func signatureB64*(sig: JwsSignature): string = sig.signatureB64
|
||||
|
||||
func initJwsSignature*(
|
||||
header: JoseHeader = initJoseHeader(alg = none[JwtAlgorithm]()),
|
||||
protected: JoseHeader,
|
||||
signatureB64: string
|
||||
): JwsSignature =
|
||||
|
||||
if not header.alg.isSome and not protected.alg.isSome:
|
||||
raise newException(ValueError,
|
||||
"alg header parameter is required but not present in either the " &
|
||||
"protected or unprotected headers.")
|
||||
|
||||
return JwsSignature(
|
||||
header: header,
|
||||
protected: protected,
|
||||
signatureB64: signatureB64)
|
||||
|
||||
proc initJwsSignature*(n: JsonNode): JwsSignature =
|
||||
return JwsSignature(
|
||||
header: initJoseHeader(parseJson(n.reqStrVal("header"))),
|
||||
protected: initJoseHeader(n.reqStrVal("protected")),
|
||||
signatureB64: n.reqStrVal("signature"))
|
||||
|
||||
func `%`*(sig: JwsSignature): JsonNode =
|
||||
%*{
|
||||
"protected": sig.protected.rawB64,
|
||||
"header": sig.header.rawB64,
|
||||
"signature": sig.signatureB64
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
# jwt_full/jwt.nim
|
||||
# Copyright 2021 Jonathan Bernard
|
||||
|
||||
## ===============================
|
||||
## jwt
|
||||
## ===============================
|
||||
##
|
||||
## -------------------------------
|
||||
## JSON Web Token (JWT) - RFC 7519
|
||||
## -------------------------------
|
||||
##
|
||||
##
|
||||
|
||||
import std/options, std/strutils, std/times
|
||||
|
||||
import ./claims, ./joseheader, ./jwa, ./jwe, ./jwk, ./jws, ./jwssig
|
||||
|
||||
export claims
|
||||
|
||||
type
|
||||
InvalidToken* = object of CatchableError
|
||||
|
||||
JwtKind = enum jkJWE, jkJWS
|
||||
|
||||
JWT* = object
|
||||
claims: JwtClaims
|
||||
case kind: JwtKind
|
||||
of jkJWE: jwe: JWE
|
||||
of jkJWS: jws: JWS
|
||||
|
||||
# Public read-only accessors to JWT members
|
||||
# -----------------------------------------
|
||||
|
||||
func claims*(jwt: JWT): JwtClaims = jwt.claims
|
||||
|
||||
func header*(jwt: JWT): JoseHeader =
|
||||
case jwt.kind:
|
||||
of jkJWS: return jwt.jws[0].protected
|
||||
of jkJWE: return jwt.jwe.header
|
||||
|
||||
func `$`*(jwt: JWT): string =
|
||||
case jwt.kind:
|
||||
of jkJWS:
|
||||
result = jwt.header.rawB64 & "." & jwt.claims.rawB64 & "."
|
||||
if jwt.jws.len > 0: result &= jwt.jws[0].signatureB64
|
||||
of jkJWE:
|
||||
result = jwt.header.rawB64 & "." & jwt.claims.rawB64 & "."
|
||||
|
||||
proc initJWT*(encoded: string): JWT =
|
||||
let parts = encoded.split('.')
|
||||
|
||||
if parts.len == 3:
|
||||
let jws = initJWS(encoded)
|
||||
result = JWT(
|
||||
kind: jkJWS,
|
||||
claims: initJwtClaims(jws.payloadB64),
|
||||
jws: jws)
|
||||
else:
|
||||
# TODO
|
||||
raise newException(Exception, "not yet implemented")
|
||||
|
||||
proc toJWT*(encoded: string): JWT = initJWT(encoded)
|
||||
|
||||
proc createSignedJwt*(
|
||||
header: JoseHeader,
|
||||
claims: JwtClaims,
|
||||
key: JWK
|
||||
): JWT =
|
||||
## Create a new JWT with the given header, claims, and signed with the given
|
||||
## key.
|
||||
|
||||
result = JWT(
|
||||
kind: jkJWS,
|
||||
claims: claims,
|
||||
jws: sign(claims, header, key))
|
||||
|
||||
proc validateTimeClaims*(jwt: JWT) =
|
||||
let now = now()
|
||||
if jwt.claims.exp.isSome and now > jwt.claims.exp.get:
|
||||
raise newException(InvalidToken, "Token is expired.")
|
||||
|
||||
if jwt.claims.nbf.isSome and now < jwt.claims.nbf.get:
|
||||
raise newException(InvalidToken, "Token cannot be used yet.")
|
||||
|
||||
proc validate*(
|
||||
jwt: JWT,
|
||||
sigAlg: JwtAlgorithm,
|
||||
key: JWK,
|
||||
validateTimeClaims = true
|
||||
) =
|
||||
|
||||
case jwt.kind:
|
||||
of jkJWS:
|
||||
jwt.jws.validate(sigAlg, key)
|
||||
|
||||
of jkJWE:
|
||||
raise newException(Exception, "JWE validation is not yet implemented")
|
||||
|
||||
if validateTimeClaims: jwt.validateTimeClaims
|
||||
|
||||
proc tryValidate*(
|
||||
jwt: JWT,
|
||||
sigAlg: JwtAlgorithm,
|
||||
key: JWK,
|
||||
validateTimeClaims = true
|
||||
): bool =
|
||||
|
||||
try:
|
||||
jwt.validate(sigAlg, key)
|
||||
return true
|
||||
except: return false
|
||||
|
||||
## proc verify*(jwt: JWT, secret: string, alg: SignatureAlgorithm): bool =
|
||||
## tryValidate(jwt, sigAlg, secret)
|
||||
|
||||
# proc sign*(jwt: JWT, key: JWK): JWT =
|
||||
@@ -0,0 +1,50 @@
|
||||
import std/options
|
||||
import bearssl
|
||||
|
||||
import ../jwa
|
||||
import ../jwk
|
||||
|
||||
import ./crypto/ecdsa
|
||||
import ./crypto/hmac
|
||||
import ./crypto/rsa
|
||||
|
||||
proc validateAlgMatchesKey(alg: JwtAlgorithm, key: JWK) =
|
||||
if key.alg.isSome and key.alg.get != $alg:
|
||||
raise newException(ValueError,
|
||||
"requested signature algorithm (" & $alg & ") does not match the " &
|
||||
"algorithm intended for use with this key (" & key.alg.get & ")")
|
||||
|
||||
proc computeSignature*(
|
||||
payload: string,
|
||||
alg: JwtAlgorithm,
|
||||
key: JWK | string
|
||||
): string =
|
||||
|
||||
if typeof(key) is JWK: validateAlgMatchesKey(alg, cast[JWK](key))
|
||||
|
||||
case alg:
|
||||
of HS256, HS384, HS512: return hmac(payload, alg, key)
|
||||
of RS256, RS384, RS512: return rsaSign(payload, alg, key)
|
||||
of ES256, ES384, ES512: return ecSign(payload, alg, key)
|
||||
else:
|
||||
raise newException(Exception,
|
||||
"unsupported signature algorithm: " & $alg)
|
||||
|
||||
proc verifySignature*(
|
||||
payload: string,
|
||||
signature: string,
|
||||
alg: JwtAlgorithm,
|
||||
key: JWK | string
|
||||
): bool =
|
||||
|
||||
if typeof(key) is JWK: validateAlgMatchesKey(alg, cast[JWK](key))
|
||||
|
||||
case alg:
|
||||
of HS256, HS384, HS512:
|
||||
let hash = hmac(payload, alg, key)
|
||||
return hash == signature
|
||||
of RS256, RS384, RS512: return rsaVerify(payload, signature, alg, key)
|
||||
of ES256, ES384, ES512: return ecVerify(payload, signature, alg, key)
|
||||
else:
|
||||
raise newException(Exception,
|
||||
"unsupported signature algorithm: " & $alg)
|
||||
@@ -0,0 +1,184 @@
|
||||
import std/tables
|
||||
import bearssl, bearssl_pkey_decoder
|
||||
|
||||
import ../../jwa
|
||||
import ../../jwk
|
||||
|
||||
import ../encoding
|
||||
|
||||
import ./hash
|
||||
import ./pem
|
||||
|
||||
type
|
||||
EcPublicKeyObj = object
|
||||
curve*: EcCurve
|
||||
q*: string
|
||||
bearKey: EcPublicKey
|
||||
|
||||
EcPrivateKeyObj = object
|
||||
curve*: EcCurve
|
||||
x*: string
|
||||
bearKey: EcPrivateKey
|
||||
|
||||
func toBearSslCurveConst(curve: EcCurve): int32 =
|
||||
result = case curve:
|
||||
of P256: EC_secp256r1
|
||||
of P384: EC_secp384r1
|
||||
of P521: EC_secp521r1
|
||||
|
||||
func fromBearSslCurveConst(curve: int32): EcCurve =
|
||||
result = case curve:
|
||||
of EC_secp256r1: P256
|
||||
of EC_secp384r1: P384
|
||||
of EC_secp521r1: P521
|
||||
else: P256
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
proc toEcPublicKey(jwk: JWK): EcPublicKeyObj =
|
||||
## Convert an ECDSA public key in JWK format to the wrapper for BearSSL's
|
||||
## ECPublicKey struct.
|
||||
|
||||
# Confusingly, the JWK standard and BearSSL use contradictory names for EC
|
||||
# key components. In JWK parlance, x and y (optional) are the public key
|
||||
# components. BearSSL only requires one public point, and calls this q.
|
||||
|
||||
let keyObj = if jwk.keyKind == EcPublic: jwk.ecPub
|
||||
else: jwk.ecPrv
|
||||
|
||||
return initEcPublicKeyObj(
|
||||
curve = keyObj.crv,
|
||||
q = b64UrlDecode(keyObj.x))
|
||||
|
||||
proc toEcPublicKey(pem: string): EcPublicKeyObj =
|
||||
var pkCtx: PkeyDecoderContext
|
||||
pkeyDecoderInit(addr pkCtx)
|
||||
decodePem(pem, "PUBLIC KEY", addr pkCtx,
|
||||
cast[BearPemDecoderCallback](pkeyDecoderPush))
|
||||
|
||||
let k = pkCtx.key.ec
|
||||
return initEcPublicKeyObj(k.curve.fromBearSslCurveConst, $cstring(k.q))
|
||||
|
||||
proc toEcPrivateKey(jwk: JWK): EcPrivateKeyObj =
|
||||
## Convert an ECDSA private key in JWK format to the wrapper for BearSSL's
|
||||
## ECPrivateKey struct.
|
||||
|
||||
# Confusingly, the JWK standard and BearSSL use contradictory names for EC
|
||||
# key components. In JWK parlance, d is the private key components. BearSSL
|
||||
# calls this x.
|
||||
return initEcPrivateKeyObj(
|
||||
curve = jwk.ecPrv.crv,
|
||||
x = b64UrlDecode(jwk.ecPrv.d))
|
||||
|
||||
proc toEcPrivateKey(pem: string): EcPrivateKeyObj =
|
||||
var skCtx: SkeyDecoderContext
|
||||
skeyDecoderInit(addr skCtx)
|
||||
decodePem(pem, "EC PRIVATE KEY", addr skCtx,
|
||||
cast[BearPemDecoderCallback](skeyDecoderPush))
|
||||
|
||||
let k = skCtx.key.ec
|
||||
return initEcPrivateKeyObj(k.curve.fromBearSslCurveConst, $cstring(k.x))
|
||||
|
||||
proc getEcHashCfg(alg: JwtAlgorithm): HashCfg =
|
||||
let hashAlg = case alg:
|
||||
of ES256: SHA256
|
||||
of ES384: SHA384
|
||||
of ES512: SHA512
|
||||
else: raise newException(ValueError,
|
||||
"Unsupported ECDSA signature algorithm '" & $alg & "'")
|
||||
|
||||
result = HASHES[hashAlg]
|
||||
|
||||
proc bearEcSign(
|
||||
message: string,
|
||||
alg: JwtAlgorithm,
|
||||
key: EcPrivateKeyObj
|
||||
): string =
|
||||
|
||||
let hashCfg = getEcHashCfg(alg)
|
||||
let hashed = hash(message, hashcfg.alg)
|
||||
|
||||
let ecSignImpl = ecdsaSignRawGetDefault()
|
||||
|
||||
# The signature fucntion will return the length of the signature in bytes,
|
||||
# but we need to pre-allocate the space for it to write into. Per BearSSL
|
||||
# documentation, the maximum signature size will by 132 bytes (see
|
||||
# https://bearssl.org/apidoc/bearssl__ec_8h.html#ab7154b0c899ceb3af062c69016258667)
|
||||
result = newString(132)
|
||||
|
||||
let sigLen = ecSignImpl(
|
||||
addr ecAllM15,
|
||||
hashCfg.vtable,
|
||||
cast[ptr char](unsafeAddr hashed[0]),
|
||||
unsafeAddr key.bearKey,
|
||||
cast[ptr char](addr result[0]))
|
||||
|
||||
if sigLen == 0: raise newException(Exception, "EC signature failed")
|
||||
result.setLen(sigLen)
|
||||
|
||||
proc bearEcVerify(
|
||||
message, signature: string,
|
||||
alg: JwtAlgorithm,
|
||||
key: EcPublicKeyObj
|
||||
): bool =
|
||||
|
||||
let hashCfg = getEcHashCfg(alg)
|
||||
let hashed = hash(message, hashCfg.alg)
|
||||
|
||||
let ecVerifyImpl = ecdsaVrfyRawGetDefault()
|
||||
let resultCode = ecVerifyImpl(
|
||||
addr ecAllM15,
|
||||
cast[ptr char](unsafeAddr hashed[0]),
|
||||
hashed.len,
|
||||
unsafeAddr key.bearKey,
|
||||
cast[ptr char](unsafeAddr signature[0]),
|
||||
signature.len)
|
||||
|
||||
result = resultCode == 1
|
||||
|
||||
proc ecSign*(message: string, alg: JwtAlgorithm, key: JWK): string =
|
||||
## Sign a message using the ECDSA algorithm.
|
||||
##
|
||||
## *key* is expected to be a `JWK <,./../jwt_full/jwk.html#JWK>`_
|
||||
|
||||
if key.keyKind != JwkKeyType.EcPrivate:
|
||||
raise newException(ValueError,
|
||||
$alg & " requires an ECDSA private key (\"type\"=\"EC\"), not a " &
|
||||
"\"typ\"=\"" & $(key.keyKind) & "\" key.")
|
||||
|
||||
return bearEcSign(message, alg, toEcPrivateKey(key))
|
||||
|
||||
proc ecSign*(message: string, alg: JwtAlgorithm, pemKey: string): string =
|
||||
## Sign a message using the ECDSA algorithm.
|
||||
##
|
||||
## *pemKey* is expected to be a PEM
|
||||
return bearEcSign(message, alg, toEcPrivateKey(pemKey))
|
||||
|
||||
proc ecVerify*(message, signature: string, alg: JwtAlgorithm, key: JWK): bool =
|
||||
## Verify the signature for a message using ECDSA.
|
||||
##
|
||||
## *key* is expected to be a `JWK <../../jwt_full/jwk.html#JWK>`_
|
||||
|
||||
if key.keyKind != JwkKeyType.EcPrivate and
|
||||
key.keyKind != JwkKeyType.EcPublic:
|
||||
raise newException(ValueError,
|
||||
$alg & " requires an ECDSA private key (\"type\"=\"EC\"), not a " &
|
||||
"\"typ\"=\"" & $(key.keyKind) & "\" key.")
|
||||
|
||||
return bearEcVerify(message, signature, alg, toEcPublicKey(key))
|
||||
|
||||
proc ecVerify*(message, signature: string, alg: JwtAlgorithm, pemKey: string): bool =
|
||||
## Verify the signature for a message using ECDSA.
|
||||
##
|
||||
## *pemKey* is expected to be a PEM.
|
||||
return bearEcVerify(message, signature, alg, toEcPublicKey(pemKey))
|
||||
@@ -0,0 +1,44 @@
|
||||
import std/tables
|
||||
import bearssl
|
||||
|
||||
type
|
||||
HashAlgorithm* = enum SHA1, SHA256, SHA384, SHA512
|
||||
|
||||
HashCfg* = object
|
||||
alg*: HashAlgorithm
|
||||
vtable*: ptr HashClass
|
||||
oid*: cstring
|
||||
size*: int
|
||||
|
||||
let HASHES* = newTable[HashAlgorithm, HashCfg]([
|
||||
(SHA1, HashCfg(
|
||||
alg: SHA1,
|
||||
vtable: addr sha1Vtable,
|
||||
oid: HASH_OID_SHA1,
|
||||
size: sha1Size)),
|
||||
(SHA256, HashCfg(
|
||||
alg: SHA256,
|
||||
vtable: addr sha256Vtable,
|
||||
oid: HASH_OID_SHA256,
|
||||
size: sha256Size)),
|
||||
(SHA384, HashCfg(
|
||||
alg: SHA384,
|
||||
vtable: addr sha384Vtable,
|
||||
oid: HASH_OID_SHA384,
|
||||
size: sha384Size)),
|
||||
(SHA512, HashCfg(
|
||||
alg: SHA512,
|
||||
vtable: addr sha512Vtable,
|
||||
oid: HASH_OID_SHA512,
|
||||
size: sha512Size)),
|
||||
])
|
||||
|
||||
proc hash*(data: string, alg: HashAlgorithm): string =
|
||||
|
||||
var ctx: HashCompatContext
|
||||
var pCtx = cast[ptr ptr HashClass](addr ctx)
|
||||
let hashCfg = HASHES[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])
|
||||
@@ -0,0 +1,47 @@
|
||||
import std/logging
|
||||
|
||||
import bearssl
|
||||
|
||||
import ../../jwa
|
||||
import ../../jwk
|
||||
|
||||
import ../encoding
|
||||
|
||||
proc bearHMAC(message: string, alg: JwtAlgorithm, key: string): string =
|
||||
|
||||
var vtable: ptr HashClass
|
||||
var hashSize: int
|
||||
|
||||
case alg:
|
||||
of HS256: vtable = addr sha256Vtable; hashSize = sha256Size
|
||||
of HS384: vtable = addr sha384Vtable; hashSize = sha384Size
|
||||
of HS512: vtable = addr sha512Vtable; hashSize = sha512Size
|
||||
else: raise newException(ValueError,
|
||||
"Unsupported HMAC algorithm '" & $alg & "'")
|
||||
|
||||
if key.len < hashSize:
|
||||
warn( "Computing an HMAC with a small key (" & $key.len & " bytes). It " &
|
||||
"is recommended to use keys of at least " & $hashSize & " bytes for " &
|
||||
$alg)
|
||||
|
||||
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)
|
||||
|
||||
let resLen = hmacSize(addr hmacCtx)
|
||||
result = newString(resLen)
|
||||
discard hmacOut(addr hmacCtx, addr result[0])
|
||||
|
||||
proc hmac*(message: string, alg: JwtAlgorithm, key: string): string =
|
||||
return bearHMAC(message, alg, key)
|
||||
|
||||
proc hmac*(message: string, alg: JwtAlgorithm, key: JWK): string =
|
||||
if key.keyKind != Octet:
|
||||
raise newException(ValueError,
|
||||
$alg & " requires an octet key (\"typ\"=\"oct\"), not a \"typ\"=\"" &
|
||||
$(key.keyKind) & "\" key.")
|
||||
|
||||
return bearHMAC(message, alg, b64UrlDecode(key.octKey.k))
|
||||
@@ -0,0 +1,108 @@
|
||||
import bearssl
|
||||
|
||||
# Taken from nim-bearssl/decls.nim
|
||||
{.pragma: bearSslFunc, cdecl, gcsafe, noSideEffect, raises: [].}
|
||||
|
||||
type BearPemDecoderCallback* = proc(keyCtx: pointer, data: pointer, dataLen: int) {.bearSslFunc.}
|
||||
|
||||
proc decodePem*(
|
||||
pem: string,
|
||||
expectedObjectName: string,
|
||||
keyCtx: pointer,
|
||||
callback: BearPemDecoderCallback
|
||||
) =
|
||||
|
||||
var pemCtx: PemDecoderContext
|
||||
pemDecoderInit(addr pemCtx)
|
||||
|
||||
var offset = 0
|
||||
var bytesRemaining = len(pem)
|
||||
var readingObj = false
|
||||
|
||||
while bytesRemaining > 0:
|
||||
let bytesRead = pemDecoderPush(addr pemCtx, unsafeAddr pem[offset], bytesRemaining)
|
||||
offset += bytesRead
|
||||
bytesRemaining -= bytesRead
|
||||
|
||||
case pemDecoderEvent(addr pemCtx):
|
||||
of PEM_BEGIN_OBJ:
|
||||
if readingObj:
|
||||
raise newException(ValueError,
|
||||
"Invalid PEM: saw a second BEGIN before seeing END.")
|
||||
|
||||
if pemDecoderName(addr pemCtx) != expectedObjectName:
|
||||
raise newException(ValueError,
|
||||
"Invalid PEM: expected BEGIN " & expectedObjectName &
|
||||
" but got BEGIN " & $pemDecoderName(addr pemCtx))
|
||||
|
||||
readingObj = true
|
||||
pemDecoderSetdest(addr pemCtx, callback, keyCtx)
|
||||
|
||||
of PEM_END_OBJ:
|
||||
if readingObj: readingObj = false
|
||||
|
||||
of PEM_ERROR: raise newException(ValueError, "Invalid PEM.")
|
||||
|
||||
else: continue
|
||||
|
||||
#[
|
||||
proc decodeKeyFromPem*(pem: string): JWK =
|
||||
var decoderCtx: PemDecoderContext
|
||||
pemDecoderInit(addr decoderCtx)
|
||||
|
||||
var bytesRead = 0
|
||||
var skCtx: SkeyDecoderContext
|
||||
var pkCtx: PkeyDecoderContext
|
||||
var keyCtx: pointer = nil
|
||||
var keyType: JwkKeyType
|
||||
|
||||
while bytesRead < len(pem):
|
||||
bytesRead += pemDecoderPush(
|
||||
addr decoderCtx,
|
||||
unsafeAddr pem[bytesRead],
|
||||
len(pem))
|
||||
|
||||
case pemDecoderEvent(addr decoderCtx):
|
||||
of PEM_BEGIN_OBJ:
|
||||
if keyCtx != nil:
|
||||
raise newException(ValueError, "Invalid PEM: saw a second BEGIN")
|
||||
|
||||
let objName = pemDecoderName(addr decoderCtx)
|
||||
if objName == "RSA PRIVATE KEY":
|
||||
skeyDecoderInit(addr skCtx)
|
||||
keyCtx = addr skCtx
|
||||
keyType = JwkKeyType.RsaPrivate
|
||||
if objName == "EC PRIVATE KEY":
|
||||
skeyDecoderInit(addr skCtx)
|
||||
keyCtx = addr skCtx
|
||||
keyType = JwkKeyType.EcPrivate
|
||||
elif objName == "RSA PUBLIC KEY":
|
||||
pkeyDecoderInit(addr pkCtx)
|
||||
keyCtx = addr pkCtx
|
||||
keyType = JwkKeyType.RsaPublic
|
||||
else:
|
||||
raise newException(ValueError,
|
||||
"Unrecognized PEM object: " & $pemDecoderName(addr decoderCtx))
|
||||
|
||||
of PEM_END_OBJ:
|
||||
if keyCtx == nil:
|
||||
raise newException(ValueError, "Saw END before BEGIN.")
|
||||
elif keyType == JwkKeyType.RsaPrivate:
|
||||
return nil
|
||||
|
||||
of PEM_ERROR:
|
||||
raise newException(ValueError, "Invalid PEM.")
|
||||
|
||||
else: discard nil
|
||||
|
||||
proc decodeRsaPem*(pem: string): JWK =
|
||||
var skCtx: SkeyDecoderContext
|
||||
skeyDecoderInit(addr skCtx)
|
||||
decodePem(
|
||||
pem,
|
||||
cast[proc(ctx: pointer, data: pointer, dataLen: int) {.bearSslFunc.}](skeyDecoderPush)
|
||||
addr skCtx)
|
||||
if skeyDecoderLastError(addr skCtx) != 0:
|
||||
raise newException(ValueError,
|
||||
"Provided PEM could not be decoded as a valid RSA private key.")
|
||||
]#
|
||||
@@ -0,0 +1,217 @@
|
||||
import std/options, std/tables
|
||||
import bearssl, bearssl_pkey_decoder
|
||||
|
||||
import ../../jwa
|
||||
import ../../jwk
|
||||
|
||||
import ../encoding
|
||||
|
||||
import ./hash
|
||||
import ./pem
|
||||
|
||||
type
|
||||
RsaPublicKeyObj = object
|
||||
n*, e*: string
|
||||
bearKey: RsaPublicKey
|
||||
|
||||
RsaPrivateKeyObj = object
|
||||
p*, q*, dp*, dq*, iq*: string
|
||||
bearKey: RsaPrivateKey
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
proc toRsaPublicKey(jwk: JWK): RsaPublicKeyObj =
|
||||
## Convert an RSA public key in JWK format to the wrapper for BearSSL's
|
||||
## RsaPublicKey struct.
|
||||
|
||||
if jwk.keyKind != JwkKeyType.RsaPublic and
|
||||
jwk.keyKind != JwkKeyType.RsaPrivate:
|
||||
raise newException(ValueError,
|
||||
"Can not extract RSA public key from the given JWT: keyKind is " &
|
||||
$jwk.keyKind)
|
||||
|
||||
return initRsaPublicKeyObj(
|
||||
n = b64UrlDecode(jwk.rsaPub.n),
|
||||
e = b64UrlDecode(jwk.rsaPub.e))
|
||||
|
||||
|
||||
proc toRsaPublicKey(pem: string): RsaPublicKeyObj =
|
||||
var pkCtx: PkeyDecoderContext
|
||||
pkeyDecoderInit(addr pkCtx)
|
||||
decodePem(pem, "PUBLIC KEY", addr pkCtx,
|
||||
cast[BearPemDecoderCallback](pkeyDecoderPush))
|
||||
|
||||
let k = pkCtx.key.rsa
|
||||
return initRsaPublicKeyObj($cstring(k.n), $cstring(k.e))
|
||||
|
||||
|
||||
proc toRsaPrivateKey(jwk: JWK): RsaPrivateKeyObj =
|
||||
## Convert an RSA private key in JWK format to the wrapper for BearSSL's
|
||||
## RsaPrivateKey struct.
|
||||
##
|
||||
## Note: there are two ways to represent an rsa private key[1][rsa-priv-rep]:
|
||||
## 1. The pair modulus and private exponent pair (*n*, *d*)
|
||||
## 2. The quintuple with the prime factors, reduced CRT exponents, and first
|
||||
## CRT coefficient (*p*, *q*, *dP*, *dQ*, *qInv*)
|
||||
##
|
||||
## The JWA standard requires that a JWK representing an RSA private key
|
||||
## provide rep. 1 (*n*, *d*) and optionally allows rep. 2 to allows be
|
||||
## included. BearSSL strictly uses rep. 2 in it's key structures.
|
||||
## **Currently, this library only supports keys that provide the parameters
|
||||
## for rep. 2.** Ideally this library would also support keys only containing
|
||||
## rep. 1, including the logic to derive rep. 2 from use in BearSSL. This is
|
||||
## a TODO.
|
||||
##
|
||||
## [NIST Special Publication 800-56B][NIST.SP.800-56Br2] provides a choice of
|
||||
## two algorithms to derive the prime factors *p* and *q* from the private
|
||||
## exponent *d*. With the prime factors known, the calculation *dP*, *dQ*,
|
||||
## and *qInv* are straighforward based on [their definitions in RFC8
|
||||
## 8017][rsa-apdx-a]
|
||||
##
|
||||
## [rsa-priv-rep]: https://datatracker.ietf.org/doc/html/rfc8017#section-3.2
|
||||
## [rsa-apdx-a]: https://datatracker.ietf.org/doc/html/rfc8017#appendix-A.1.2
|
||||
## [NIST.SP.800-56Br2]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Br2.pdf
|
||||
|
||||
if jwk.keyKind != JwkKeyType.RsaPrivate:
|
||||
raise newException(ValueError,
|
||||
"Can not extract RSA private key from the given JWT: keyKind is " &
|
||||
$jwk.keyKind)
|
||||
|
||||
let sk = jwk.rsaPrv
|
||||
if sk.p.isNone or sk.q.isNone or sk.dp.isNone or sk.dq.isNone or sk.qi.isNone:
|
||||
raise newException(ValueError,
|
||||
"RSA private key must have values for: n, e, d, p, q, dp, dq, and qi")
|
||||
|
||||
let n = b64UrlDecode(sk.n)
|
||||
return initRsaPrivateKeyObj(
|
||||
nBitLen = n.len * 8,
|
||||
p = b64UrlDecode(sk.p.get),
|
||||
q = b64UrlDecode(sk.q.get),
|
||||
dp = b64UrlDecode(sk.dp.get),
|
||||
dq = b64UrlDecode(sk.dq.get),
|
||||
iq = b64UrlDecode(sk.qi.get))
|
||||
|
||||
proc toRsaPrivateKey(pem: string): RsaPrivateKeyObj =
|
||||
var skCtx: SkeyDecoderContext
|
||||
skeyDecoderInit(addr skCtx)
|
||||
decodePem(pem, "RSA PRIVATE KEY", addr skCtx,
|
||||
cast[BearPemDecoderCallback](skeyDecoderPush))
|
||||
|
||||
let k = skCtx.key.rsa
|
||||
|
||||
return initRsaPrivateKeyObj(
|
||||
cast[int](k.nBitLen),
|
||||
$cstring(k.p),
|
||||
$cstring(k.q),
|
||||
$cstring(k.dp),
|
||||
$cstring(k.dq),
|
||||
$cstring(k.iq))
|
||||
|
||||
proc getRsaHashCfg(alg: JwtAlgorithm): HashCfg =
|
||||
let hashAlg = case alg:
|
||||
of RS256: SHA256
|
||||
of RS384: SHA384
|
||||
of RS512: SHA512
|
||||
else: raise newException(ValueError,
|
||||
"Unsupported RSA signature algorithm '" & $alg & "'")
|
||||
|
||||
result = HASHES[hashAlg]
|
||||
|
||||
proc bearRsaSign(
|
||||
message: string,
|
||||
alg: JwtAlgorithm,
|
||||
key: RsaPrivateKeyObj
|
||||
): string =
|
||||
|
||||
let hashCfg = getRsaHashCfg(alg)
|
||||
let hashed = hash(message, hashCfg.alg)
|
||||
|
||||
let rsaSignImpl = rsaPkcs1SignGetDefault()
|
||||
result = newString((key.bearKey.nBitLen + 7) div 8)
|
||||
|
||||
let errCode = rsaSignImpl(
|
||||
cast[ptr char](hashCfg.oid),
|
||||
cast[ptr char](unsafeAddr hashed[0]),
|
||||
hashed.len,
|
||||
unsafeAddr key.bearKey,
|
||||
cast[ptr char](addr result[0]))
|
||||
|
||||
if errCode != 1: raise newException(Exception, "RSA signature failed")
|
||||
|
||||
proc bearRsaVerify(
|
||||
message, signature: string,
|
||||
alg: JwtAlgorithm,
|
||||
key: RsaPublicKeyObj
|
||||
): bool =
|
||||
|
||||
let hashCfg = getRsaHashCfg(alg)
|
||||
let hashed = hash(message, hashCfg.alg)
|
||||
|
||||
let rsaVerifyImpl = rsaPkcs1VrfyGetDefault()
|
||||
var recoveredHash = newString(hashCfg.size)
|
||||
|
||||
let errCode = rsaVerifyImpl(
|
||||
cast[ptr char](unsafeAddr signature[0]),
|
||||
signature.len,
|
||||
cast[ptr char](hashCfg.oid),
|
||||
hashed.len,
|
||||
unsafeAddr key.bearKey,
|
||||
cast[ptr char](addr recoveredHash[0]))
|
||||
|
||||
if errCode != 1: return false
|
||||
return hashed == recoveredHash
|
||||
|
||||
proc rsaSign*(message: string, alg: JwtAlgorithm, key: JWK): string =
|
||||
## Sign a message using the RSA PKCS#1 v1.5 algorithm.
|
||||
##
|
||||
## *key* is expected to be a `JWK <../../jwt_full/jwk.html#JWK>`_
|
||||
|
||||
if key.keyKind != JwkKeyType.RsaPrivate:
|
||||
raise newException(ValueError,
|
||||
$alg & " requires an RSA Private key (\"type\"=\"RSA\"), not a " &
|
||||
"\"typ\"=\"" & $(key.keyKind) & "\" key.")
|
||||
|
||||
return bearRsaSign(message, alg, toRsaPrivateKey(key))
|
||||
|
||||
proc rsaSign*(message: string, alg: JwtAlgorithm, pemKey: string): string =
|
||||
## Sign a message using hte RSA PKCS#1 v1.5 algorithm.
|
||||
##
|
||||
## *pemKey* is expected to be an RSA private key in PEM format.
|
||||
return bearRsaSign(message, alg, toRsaPrivateKey(pemKey))
|
||||
|
||||
proc rsaVerify*(message, signature: string; alg: JwtAlgorithm, key: JWK): bool =
|
||||
## Verify the signature for a message using PKCS#1 v1.5 algorithm.
|
||||
##
|
||||
## *key* is expected to be a `JWK <../../jwt_full/jwk.html#JWK>`_
|
||||
|
||||
if key.keyKind != JwkKeyType.RsaPublic:
|
||||
raise newException(ValueError,
|
||||
$alg & " requires an RSA Public key (\"type\"=\"RSA\"), not a " &
|
||||
"\"typ\"=\"" & $(key.keyKind) & "\" key.")
|
||||
|
||||
return bearRsaVerify(message, signature, alg, toRsaPublicKey(key))
|
||||
|
||||
proc rsaVerify*(message, signature: string; alg: JwtAlgorithm, pemKey: string): bool =
|
||||
## Verify the signature for a message using PKCS#1 v1.5 algorithm.
|
||||
##
|
||||
## *key* is expected to be an RSA public key in PEM format.
|
||||
return bearRsaVerify(message, signature, alg, toRsaPublicKey(pemKey))
|
||||
@@ -0,0 +1,22 @@
|
||||
import std/base64, std/strutils
|
||||
|
||||
proc b64UrlEncode*(s: string): string =
|
||||
result = encode(s, safe = true)
|
||||
while result.endsWith("="):
|
||||
result.setLen(result.len - 1)
|
||||
|
||||
proc b64UrlEncode*[T: SomeInteger | char](arr: openarray[T]): string =
|
||||
result= encode(arr, safe = true)
|
||||
while result.endsWith("="):
|
||||
result.setLen(result.len - 1)
|
||||
|
||||
proc b64UrlDecode*(s: string): string =
|
||||
var withPadding = s
|
||||
while withPadding.len mod 4 > 0:
|
||||
withPadding &= "="
|
||||
result = decode(withPadding)
|
||||
|
||||
func byteArrToString*[T: SomeInteger | char](arr: openarray[T]): string =
|
||||
result = newString(arr.len)
|
||||
for i in 0..<arr.len:
|
||||
result[i] = cast[char](arr[i])
|
||||
@@ -0,0 +1,34 @@
|
||||
import std/json, std/options, std/strutils, std/times
|
||||
|
||||
import ./timeutils
|
||||
|
||||
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]()
|
||||
|
||||
func reqIntVal*(n: JsonNode, key: string): BiggestInt =
|
||||
if n.hasKey(key):
|
||||
if n[key].kind == JInt:
|
||||
return n[key].getBiggestInt
|
||||
|
||||
raise newException(ValueError, "value for '" & key & "' is missing or not an integer")
|
||||
|
||||
func optIntVal*(n: JsonNode, key: string): Option[BiggestInt] =
|
||||
if n.hasKey(key) and n[key].kind == JInt:
|
||||
some(n[key].getBiggestInt)
|
||||
else: none[BiggestInt]()
|
||||
|
||||
proc reqNumericDate*(n: Jsonnode, key: string): DateTime =
|
||||
fromNumericDate(n.reqIntVal(key))
|
||||
|
||||
proc optNumericDate*(n: JsonNode, key: string): Option[DateTime] =
|
||||
let val = n.optIntVal(key)
|
||||
if val.isSome: return some(fromNumericDate(val.get))
|
||||
else: return none[DateTime]()
|
||||
@@ -0,0 +1,5 @@
|
||||
import times
|
||||
|
||||
proc fromNumericDate*(i: BiggestInt): DateTime = fromUnix(i).utc
|
||||
|
||||
proc toNumericDate*(d: DateTime): BiggestInt = toUnix(d.utc.toTime)
|
||||
Reference in new issue
Block a user