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

+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))