Rename to jwt_full to avoid collision with existing nim-jwt.

This commit is contained in:
jdb committed 2021-12-25 21:24:48 -06:00
1 parent 35a9cf682c
commit 4cc2a4dee5
25 files changed
+30 -30

No files matched your search

+109
View File
@@ -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)
+126
View File
@@ -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)
+45
View File
@@ -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"
+19
View File
@@ -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
+198
View File
@@ -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))
+253
View File
@@ -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
+55
View File
@@ -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
}
+116
View File
@@ -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 =
+50
View File
@@ -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)
+184
View File
@@ -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))
+44
View File
@@ -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])
+47
View File
@@ -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))
+108
View File
@@ -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.")
]#
+217
View File
@@ -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))
+22
View File
@@ -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])
+34
View File
@@ -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]()
+5
View File
@@ -0,0 +1,5 @@
import times
proc fromNumericDate*(i: BiggestInt): DateTime = fromUnix(i).utc
proc toNumericDate*(d: DateTime): BiggestInt = toUnix(d.utc.toTime)