diff --git a/src/main/jwt/jwa.nim b/src/main/jwt/jwa.nim index 4d199f1..174e157 100644 --- a/src/main/jwt/jwa.nim +++ b/src/main/jwt/jwa.nim @@ -28,4 +28,4 @@ type A128GCM = "A128GCM", A192GCM = "A192GCM", A256GCM = "A256GCM" - EcCrv* = enum P256 = "P-256", P384 = "P-384", P521 = "P-521" + EcCurve* = enum P256 = "P-256", P384 = "P-384", P521 = "P-521" diff --git a/src/main/jwt/jwk.nim b/src/main/jwt/jwk.nim index 2db24e6..86db528 100644 --- a/src/main/jwt/jwk.nim +++ b/src/main/jwt/jwk.nim @@ -1,4 +1,4 @@ -import std/json, std/options, std/sequtils +import std/json, std/options, std/sequtils, std/strutils import ../private/jsonutils import ./jwa @@ -14,7 +14,7 @@ type jwkopDeriveKey = "deriveKey", jwkopDeriveBits = "deriveBits" EcPubKey = object of RootObj - crv*: string + crv*: EcCurve x*: string y*: Option[string] @@ -86,12 +86,12 @@ func `[]`*(jwk: JWK, key: string): Option[JsonNode] = if jwk.json.hasKey(key): some(jwk.json[key]) else: none[JsonNode]() -## Public Parsing Functions -## ------------------------ +# Public Parsing Functions +# ------------------------ func parseEcPubKey*(n: JsonNode): EcPubKey = EcPubKey( - crv: n.reqStrVal("crv"), + crv: parseEnum[EcCurve](n.reqStrVal("crv")), x: n.reqStrVal("x"), y: n.optStrVal("y")) diff --git a/src/main/jwt/jws.nim b/src/main/jwt/jws.nim index 185ca11..ee52b09 100644 --- a/src/main/jwt/jws.nim +++ b/src/main/jwt/jws.nim @@ -1,4 +1,4 @@ -import std/json, std/options, std/sequtils, std/strutils +import std/json, std/logging, std/options, std/sequtils, std/strutils import ../private/crypto import ../private/encoding @@ -71,6 +71,8 @@ proc initJWS*(b64: string): JWS = 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( @@ -94,19 +96,23 @@ proc initJWS*(n: JsonNode): JWS = payloadB64: n.reqStrVal("payload"), signatures: @[initJwsSignature(n)]) - -proc sign*(payload: string, header: JoseHeader, key: JWK): JWS = +proc sign*(payload: string, header: JoseHeader, key: JWK, 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 = b64UrlEncode(payload) + 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: @[ @@ -120,24 +126,30 @@ proc sign*(payload: string, header: JoseHeader, key: JWK): JWS = 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) + sign($claims, header, key, payloadIsB64Encoded = true) proc sign*( payload: string, unprotected: JoseHeader, protected: JoseHeader, - key: JWK): JWS = + key: JWK, + 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 = b64UrlEncode(payload) + 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: @[ @@ -165,13 +177,16 @@ proc sign*( 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)]) + signatureB64 = signatureB64)]) result.compactSerialization = none[string]() @@ -199,8 +214,14 @@ proc validate*(jws: JWS, alg: JwtAlgorithm, key: JWK, sigIdx = 0) = 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 = $(jws[sigIdx].protected) & "." & jws.payloadB64, + payload = payload, signature = b64UrlDecode(jws[sigIdx].signatureB64), alg = alg, key = key): diff --git a/src/main/jwt/jwt.nim b/src/main/jwt/jwt.nim index baf87a6..0d5df4d 100644 --- a/src/main/jwt/jwt.nim +++ b/src/main/jwt/jwt.nim @@ -1,10 +1,12 @@ import std/json, std/options, std/strutils, std/times -import ./claims, ./joseheader, ./jwe, ./jws +import ./claims, ./joseheader, ./jwa, ./jwe, ./jwk, ./jws, ./jwssig export claims type + InvalidToken* = object of CatchableError + JwtKind = enum jkJWE, jkJWS JWT* = object @@ -20,27 +22,58 @@ func claims*(jwt: JWT): JwtClaims = jwt.claims func header*(jwt: JWT): JoseHeader = case jwt.kind: - of jkJWS: return jwt.jws.header + of jkJWS: return jwt.jws[0].protected of jkJWE: return jwt.jwe.header func `$`*(jwt: JWT): string = - result = jwt.header.rawB64 & "." & jwt.claims.rawB64 & "." - if jwt.signature.isSome: result &= jwt.signature.get + 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('.') - result = JWT( - signature: if parts.len > 2: some(parts[2]) - else: none[string](), - header: initJoseHeader(parts[0]), - claims: initJwtClaims(parts[1])) + 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. -proc initJWT*(header: JoseHeader, claims: JwtClaims): JWT = result = JWT( - header: header, + kind: jkJWS, claims: claims, - signature: none[string]()) + jws: sign(claims, header, key)) + +proc validate*(jwt: JWT, sigAlg: JwtAlgorithm, key: JWK) = + case jwt.kind: + of jkJWS: + jwt.jws.validate(sigAlg, key) + + of jkJWE: + raise newException(Exception, "JWE validation is not yet implemented") + +proc tryValidate*(jwt: JWT, sigAlg: JwtAlgorithm, key: JWK): bool = + try: + jwt.validate(sigAlg, key) + return true + except: return false # proc verify*(jwt: JWT): bool = diff --git a/src/main/private/crypto.nim b/src/main/private/crypto.nim index 14b1220..35c9a23 100644 --- a/src/main/private/crypto.nim +++ b/src/main/private/crypto.nim @@ -4,8 +4,9 @@ import bearssl import ../jwt/jwa import ../jwt/jwk -import ./crypto/hash +import ./crypto/ecdsa import ./crypto/hmac +import ./crypto/rsa proc validateAlgMatchesKey(alg: JwtAlgorithm, key: JWK) = if key.alg.isSome and key.alg.get != $alg: @@ -16,13 +17,15 @@ proc validateAlgMatchesKey(alg: JwtAlgorithm, key: JWK) = proc computeSignature*( payload: string, alg: JwtAlgorithm, - key: JWK + key: JWK | string ): string = validateAlgMatchesKey(alg, key) case alg: - of HS256, HS384, HS512: return hmac(alg, key, payload) + 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) @@ -38,8 +41,10 @@ proc verifySignature*( case alg: of HS256, HS384, HS512: - let hash = hmac(alg, key, payload) + 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) diff --git a/src/main/private/crypto/ecdsa.nim b/src/main/private/crypto/ecdsa.nim new file mode 100644 index 0000000..a8a226d --- /dev/null +++ b/src/main/private/crypto/ecdsa.nim @@ -0,0 +1,146 @@ +import std/options, std/tables +import bearssl + +import ../../jwt/jwa +import ../../jwt/jwk + +import ../encoding + +import ./hash + +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 initEcPublicKeyObj(curve: EcCurve, q: string): EcPublicKeyObj = + result = EcPublicKeyObj(curve: curve, q: q) + result.bearKey.curve = curve.toBearSslCurveConst + result.bearKey.q = cast[ptr cuchar](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 cuchar](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 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 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 cuchar](unsafeAddr hashed[0]), + unsafeAddr key.bearKey, + cast[ptr cuchar](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 cuchar](unsafeAddr hashed[0]), + hashed.len, + unsafeAddr key.bearKey, + cast[ptr cuchar](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/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 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/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)) diff --git a/src/main/private/crypto/hash.nim b/src/main/private/crypto/hash.nim index affd5b5..fc0062e 100644 --- a/src/main/private/crypto/hash.nim +++ b/src/main/private/crypto/hash.nim @@ -5,35 +5,40 @@ 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*(alg: HashAlgorithm, data: string): string = +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, data.len) + hashCfg.vtable.update(pCtx, unsafeAddr data[0], data.len) hashCfg.vtable.output(pCtx, addr result[0]) diff --git a/src/main/private/crypto/hmac.nim b/src/main/private/crypto/hmac.nim index d0189ed..c526837 100644 --- a/src/main/private/crypto/hmac.nim +++ b/src/main/private/crypto/hmac.nim @@ -7,7 +7,7 @@ import ../../jwt/jwk import ../encoding -proc bearHMAC(alg: JwtAlgorithm, key, toSign: string): string = +proc bearHMAC(message: string, alg: JwtAlgorithm, key: string): string = var vtable: ptr HashClass var hashSize: int @@ -29,19 +29,19 @@ proc bearHMAC(alg: JwtAlgorithm, key, toSign: string): string = hmacKeyInit(addr keyCtx, vtable, key.cstring, key.len) hmacInit(addr hmacCtx, addr keyCtx, 0) - hmacUpdate(addr hmacCtx, toSign.cstring, toSign.len) + hmacUpdate(addr hmacCtx, message.cstring, message.len) let resLen = hmacSize(addr hmacCtx) result = newString(resLen) discard hmacOut(addr hmacCtx, addr result[0]) -proc hmac*(alg: JwtAlgorithm, key, toSign: string): string = - return bearHMAC(alg, key, toSign) +proc hmac*(message: string, alg: JwtAlgorithm, key: string): string = + return bearHMAC(message, alg, key) -proc hmac*(alg: JwtAlgorithm, key: JWK, toSign: string): string = +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(alg, b64UrlDecode(key.octKey.k), toSign) + return bearHMAC(message, alg, b64UrlDecode(key.octKey.k)) diff --git a/src/main/private/crypto/rsa.nim b/src/main/private/crypto/rsa.nim index fe718b8..66c18ce 100644 --- a/src/main/private/crypto/rsa.nim +++ b/src/main/private/crypto/rsa.nim @@ -1,23 +1,151 @@ +import std/options, std/tables import bearssl import ../../jwt/jwa import ../../jwt/jwk -proc bearRsaSign( - alg: JwtAlgorithm, - key: RsaPrivateKey, - toSign: string - ): string = +import ../encoding - var hashVtable: ptr HashClass - var hashOID: cstring +import ./hash - case alg: - of RS256: hashVtable = addr sha256Vtable; hashOID = HASH_OID_SHA256 - of RS384: hashVtable = addr sha384Vtable; hashOID = HASH_OID_SHA384 - of RS512: hashVtable = addr sha512Vtable; hashOID = HASH_OID_SHA512 +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 cuchar](result.n.cstring) + result.bearKey.nlen = result.n.len + result.bearKey.e = cast[ptr cuchar](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 cuchar](result.p.cstring) + result.bearKey.plen = result.p.len + result.bearKey.q = cast[ptr cuchar](result.q.cstring) + result.bearKey.qlen = result.q.len + result.bearKey.dp = cast[ptr cuchar](result.dp.cstring) + result.bearKey.dplen = result.dp.len + result.bearKey.dq = cast[ptr cuchar](result.dq.cstring) + result.bearKey.dqlen = result.dq.len + result.bearKey.iq = cast[ptr cuchar](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. + return initRsaPublicKeyObj( + n = b64UrlDecode(jwk.rsaPub.n), + e = b64UrlDecode(jwk.rsaPub.e)) + +proc toRsaPrivateKey(jwk: JWK): RsaPrivateKeyObj = + ## Convert an RSA private key in JWK format to the wrapper for BearSSL's + ## RsaPrivateKey struct. + + # TODO: JWS spec only requires a private key to have n, e, and d, as the + # remainder can be computed form these (p, q, dp, dq, and qi). BearSSL + # requires p, q, dp, dq, and qi (it calls iq). Because of this, we currently + # require all values to be present in JWKs for RSA privat keys. We should add + # the logic to compute the missing values to fully support the JWS spec. + # + # We also do not currently support keys with more than two prime factors. + + 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 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 & "'") - # TODO - "" + 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 cuchar](hashCfg.oid), + cast[ptr cuchar](unsafeAddr hashed[0]), + hashed.len, + unsafeAddr key.bearKey, + cast[ptr cuchar](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 cuchar](unsafeAddr signature[0]), + signature.len, + cast[ptr cuchar](hashCfg.oid), + hashed.len, + unsafeAddr key.bearKey, + cast[ptr cuchar](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/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 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/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)) diff --git a/src/test/testdata.nim b/src/test/testdata.nim index 5ae92ba..d5eb4db 100644 --- a/src/test/testdata.nim +++ b/src/test/testdata.nim @@ -1,30 +1,85 @@ ## Example JWT from RFC 7519 (JWT) section 3.1 -const rfc7519Sec3_1ExampleHeaderB64* = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" -const rfc7519Sec3_1ExampleHeaderBytes* = [123, 34, 116, 121, 112, 34, 58, 34, 74, 87, 84, 34, 44, 13, 10, 32, 34, 97, 108, 103, 34, 58, 34, 72, 83, 50, 53, 54, 34, 125] -const rfc7519Sec3_1ExampleClaimsB64* = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" -const rfc7519Sec3_1ExampleClaimsBytes* = [123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125] +const rfc7519_S31_HeaderB64* = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" +const rfc7519_S31_HeaderBytes* = [123, 34, 116, 121, 112, 34, 58, 34, 74, 87, 84, 34, 44, 13, 10, 32, 34, 97, 108, 103, 34, 58, 34, 72, 83, 50, 53, 54, 34, 125] +const rfc7519_S31_ClaimsB64* = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" +const rfc7519_S31_ClaimsBytes* = [123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125] -## Example JWS from RFC 7515 (JWS) Appendix A.1 -const rfc7515A1ExampleHeaderB64* = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" -const rfc7515A1ExampleHeaderBytes* = [123, 34, 116, 121, 112, 34, 58, 34, 74, 87, 84, 34, 44, 13, 10, 32, 34, 97, 108, 103, 34, 58, 34, 72, 83, 50, 53, 54, 34, 125] -const rfc7515A1ExapmleClaimsB64* = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" -const rfc7515A1ExampleClaimsBytes* = [123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125] -const rfc7515A1ExampleSigB64* = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" -const rfc7515A1ExampleSigBytes* = [116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173, 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83, 132, 141, 121] -const rfc7515A1ExampleJwkStr* = """{ +## Examples from RFC 7515 (JWS +const rfc7515_A1_HeaderB64* = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" +const rfc7515_A1_HeaderBytes* = [123, 34, 116, 121, 112, 34, 58, 34, 74, 87, 84, 34, 44, 13, 10, 32, 34, 97, 108, 103, 34, 58, 34, 72, 83, 50, 53, 54, 34, 125] +const rfc7515_A1_ClaimsB64* = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" +const rfc7515_A1_ClaimsBytes* = [123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125] +const rfc7515_A1_SigB64* = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" +const rfc7515_A1_SigBytes* = [116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173, 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83, 132, 141, 121] +const rfc7515_A1_Jwt* = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" +const rfc7515_A1_JwkStr* = """{ "kty":"oct", "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow" }""" -const rfc7515A1ExampleJwt* = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + +const rfc7515_A2_HeaderB64* = "eyJhbGciOiJSUzI1NiJ9" +const rfc7515_A2_HeaderBytes* = [123, 34, 97, 108, 103, 34, 58, 34, 82, 83, 50, 53, 54, 34, 125] +const rfc7515_A2_HeaderStr* = """{"alg":"RS256"}""" +const rfc7515_A2_ClaimsB64* = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" +const rfc7515_A2_SigB64* = "cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw" +const rfc7515_A2_Jwt* = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw" +const rfc7515_A2_JwkPrvStr* = """{ + "kty":"RSA", + "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", + "e":"AQAB", + "d":"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97IjlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYTCBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLhBOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ", + "p":"4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdiYrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPGBY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc", + "q":"uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxaewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA-njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc", + "dp":"BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3QCLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0", + "dq":"h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-kyNlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU", + "qi":"IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2oy26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLUW0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U" +}""" +const rfc7515_A2_JwkPubStr* = """{ + "kty":"RSA", + "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", + "e":"AQAB" +}""" + + +const rfc7515_A3_HeaderB64* = "eyJhbGciOiJFUzI1NiJ9" +const rfc7515_A3_HeaderBytes* = [123, 34, 97, 108, 103, 34, 58, 34, 69, 83, 50, 53, 54, 34, 125] +const rfc7515_A3_HeaderStr* = """{"alg":"ES256"}""" +const rfc7515_A3_ClaimsB64* = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" +const rfc7515_A3_SignB64* = "DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q" +const rfc7515_A3_Jwt* = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q" +const rfc7515_A3_JwkStr* = """{ + "kty":"EC", + "crv":"P-256", + "x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", + "y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", + "d":"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI" +}""" + + +const rfc7515_A4_HeaderB64* = "eyJhbGciOiJFUzUxMiJ9" +const rfc7515_A4_HeaderBytes* = [123, 34, 97, 108, 103, 34, 58, 34, 69, 83, 53, 49, 50, 34, 125] +const rfc7515_A4_HeaderStr* = """{"alg":"ES512"}""" +const rfc7515_A4_PayloadB64* = "UGF5bG9hZA" +const rfc7515_A4_PayloadBytes* = [80, 97, 121, 108, 111, 97, 100] +const rfc7515_A4_SignB64* = "AdwMgeerwtHoh-l192l60hp9wAHZFVJbLfD_UxMi70cwnZOYaRI1bKPWROc-mZZqwqT2SI-KGDKB34XO0aw_7XdtAG8GaSwFKdCAPZgoXD2YBJZCPEX3xKpRwcdOO8KpEHwJjyqOgzDO7iKvU8vcnwNrmxYbSW9ERBXukOXolLzeO_Jn" +const rfc7515_A4_Jwt* = "eyJhbGciOiJFUzUxMiJ9.UGF5bG9hZA.AdwMgeerwtHoh-l192l60hp9wAHZFVJbLfD_UxMi70cwnZOYaRI1bKPWROc-mZZqwqT2SI-KGDKB34XO0aw_7XdtAG8GaSwFKdCAPZgoXD2YBJZCPEX3xKpRwcdOO8KpEHwJjyqOgzDO7iKvU8vcnwNrmxYbSW9ERBXukOXolLzeO_Jn" +const rfc7515_A4_JwkStr* = """{ + "kty":"EC", + "crv":"P-521", + "x":"AekpBQ8ST8a8VcfVOTNl353vSrDCLLJXmPk06wTjxrrjcBpXp5EOnYG_NjFZ6OvLFV1jSfS9tsz4qUxcWceqwQGk", + "y":"ADSmRA43Z1DSNx_RvcLI87cdL07l6jQyyBXMoxVg_l2Th-x3S1WDhjDly79ajL4Kkd0AZMaZmh9ubmf63e3kyMj2", + "d":"AY5pb7A0UFiB3RELSD64fTLOSV_jazdF7fLYyuTw8lOfRhWg6Y6rUrPAxerEzgdRhajnu0ferB0d53vM9mE15j2C" +}""" # Note: due to the way Nim handles newline literals, this does not exactly # match the data from the RFC. For cases where we want exact matches (to # validate B64 conversion, signature, etc.) use either the Base64 version or # the byte arrays above. -const rfc7519Sec3_1ExampleHeaderStr* = """{"typ":"JWT", "alg":"HS256"}""" -const rfc7519Sec3_1ExampleClaimsStr* = """{"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}""" -const rfc7515A1ExampleHeaderStr* = """{"typ":"JWT", "alg":"HS256"}""" -const rfc7515A1ExampleClaimsStr* = """{"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}""" +const rfc7519_S31_HeaderStr* = """{"typ":"JWT", "alg":"HS256"}""" +const rfc7519_S31_ClaimsStr* = """{"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}""" +const rfc7515_A1_HeaderStr* = """{"typ":"JWT", "alg":"HS256"}""" +const rfc7515_A1_ClaimsStr* = """{"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}""" +const rfc7515_A2_ClaimsStr* = """{"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}""" const sampleJwt* = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYmMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.fKGoqlw-Nporec-dVTrNbC0ZC7kAhUsEs50s12Iwy14" const sampleJwtHeaderDecoded* = """{"alg":"HS256","typ":"JWT"}""" diff --git a/src/test/unit/runner.nim b/src/test/unit/runner.nim index 3e5ed88..2cb5d53 100644 --- a/src/test/unit/runner.nim +++ b/src/test/unit/runner.nim @@ -1,7 +1,8 @@ -import std/unittest +import std/logging import ./tclaims import ./tencoding import ./tjoseheader import ./tjwk import ./tjws +import ./tjwt diff --git a/src/test/unit/tclaims.nim b/src/test/unit/tclaims.nim index 97f447d..965e4da 100644 --- a/src/test/unit/tclaims.nim +++ b/src/test/unit/tclaims.nim @@ -6,7 +6,7 @@ import ../testdata suite "jwt/claims": test "initClaims(b64)": - let rfcClaims = initJwtClaims(rfc7519Sec3_1ExampleClaimsB64) + let rfcClaims = initJwtClaims(rfc7519_S31_ClaimsB64) let sampleClaims = initJwtClaims(sampleJwt.split('.')[1]) check: @@ -23,7 +23,7 @@ suite "jwt/claims": sampleClaims["name"].get.getStr == "John Doe" test "round trip": - let rfcClaims1 = initJwtClaims(rfc7519Sec3_1ExampleClaimsB64) + let rfcClaims1 = initJwtClaims(rfc7519_S31_ClaimsB64) let rfcClaims2 = initJwtClaims($rfcClaims1) let sampleClaims1 = initJwtClaims(sampleJwt.split('.')[1]) diff --git a/src/test/unit/tencoding.nim b/src/test/unit/tencoding.nim index b2e875a..a63a111 100644 --- a/src/test/unit/tencoding.nim +++ b/src/test/unit/tencoding.nim @@ -7,8 +7,10 @@ suite "private/encoding": test "b64UrlEncode(int array)": check: - rfc7519Sec3_1ExampleHeaderB64 == b64UrlEncode(rfc7519Sec3_1ExampleHeaderBytes) - rfc7519Sec3_1ExampleClaimsB64 == b64UrlEncode(rfc7519Sec3_1ExampleClaimsBytes) + rfc7519_S31_HeaderB64 == b64UrlEncode(rfc7519_S31_HeaderBytes) + rfc7519_S31_ClaimsB64 == b64UrlEncode(rfc7519_S31_ClaimsBytes) + rfc7515_A1_HeaderB64 == b64UrlEncode(rfc7515_A1_HeaderBytes) + rfc7515_A1_ClaimsB64 == b64UrlEncode(rfc7515_A1_ClaimsBytes) test "b64UrlEncode(string)": let jwtParts = sampleJwt.split('.') @@ -16,12 +18,13 @@ suite "private/encoding": check: b64UrlEncode(sampleJwtHeaderDecoded) == jwtParts[0] b64UrlEncode(sampleJwtClaimsDecoded) == jwtParts[1] + rfc7515_A2_HeaderB64 == b64UrlEncode(rfc7515_A2_HeaderStr) # Nim mangles \r\n somehow, causing the below to fail. However, the # failure is at the string literal -> string var layer, not the encoding # layer) - # rfc7519Sec3_1ExampleHeaderB64 == b64UrlEncode(rfc7519Sec3_1ExampleHeaderStr) - # rfc7519Sec3_1ExampleClaimsB64 == b64UrlEncode(rfc7519Sec3_1ExampleClaimsStr) + # rfc7519_S31_HeaderB64 == b64UrlEncode(rfc7519_S31_HeaderStr) + # rfc7519_S31_ClaimsB64 == b64UrlEncode(rfc7519_S31_ClaimsStr) test "b64UrlDecode(string)": let jwtParts = sampleJwt.split('.') @@ -29,7 +32,8 @@ suite "private/encoding": check: sampleJwtHeaderDecoded == b64UrlDecode(jwtParts[0]) sampleJwtClaimsDecoded == b64UrlDecode(jwtParts[1]) + rfc7515_A2_HeaderStr == b64UrlDecode(rfc7515_A2_HeaderB64) test "byteArrToString": check: - byteArrToString(rfc7515A1ExampleHeaderBytes) == b64UrlDecode(rfc7515A1ExampleHeaderB64) + byteArrToString(rfc7515_A1_HeaderBytes) == b64UrlDecode(rfc7515_A1_HeaderB64) diff --git a/src/test/unit/tjoseheader.nim b/src/test/unit/tjoseheader.nim index 527e5c8..3342abd 100644 --- a/src/test/unit/tjoseheader.nim +++ b/src/test/unit/tjoseheader.nim @@ -8,11 +8,11 @@ import ../testdata suite "jwt/joseheader": test "initJoseHeader always sets rawB64": - let h1 = initJoseHeader(rfc7519Sec3_1ExampleHeaderB64) - let h2 = initJoseHeader(parseJson(rfc7519Sec3_1ExampleHeaderStr)) + let h1 = initJoseHeader(rfc7519_S31_HeaderB64) + let h2 = initJoseHeader(parseJson(rfc7519_S31_HeaderStr)) let h3 = initJoseHeader(alg = some(HS256), typ = some("JWT")) check: - h1.rawB64 == rfc7519Sec3_1ExampleHeaderB64 + h1.rawB64 == rfc7519_S31_HeaderB64 h2.rawB64 == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" h3.rawB64 == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" diff --git a/src/test/unit/tjwk.nim b/src/test/unit/tjwk.nim index 9622c69..cc0e441 100644 --- a/src/test/unit/tjwk.nim +++ b/src/test/unit/tjwk.nim @@ -62,7 +62,7 @@ suite "jwt/jwk": jwk.kid.isSome jwk.kid.get == "1" jwk.key_ops.isNone - jwk.ecPub.crv == $P256 + jwk.ecPub.crv == P256 jwk.ecPub.x == "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4" jwk.ecPub.y.isSome jwk.ecPub.y.get == "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM" @@ -126,7 +126,7 @@ suite "jwt/jwk": jwk.kid.isSome jwk.kid.get == "1" jwk.key_ops.isNone - jwk.ecPrv.crv == $P256 + jwk.ecPrv.crv == P256 jwk.ecPrv.x == "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4" jwk.ecPrv.y.isSome jwk.ecPrv.y.get == "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM" diff --git a/src/test/unit/tjws.nim b/src/test/unit/tjws.nim index 99c65af..afad221 100644 --- a/src/test/unit/tjws.nim +++ b/src/test/unit/tjws.nim @@ -1,6 +1,6 @@ import std/json, std/unittest -import jwt/claims, jwt/joseheader, jwt/jwa, jwt/jwk, jwt/jws, jwt/jwssig +import jwt/claims, jwt/joseheader, jwt/jwa, jwt/jwk, jwt/jws import private/encoding @@ -8,12 +8,19 @@ import ../testdata suite "jwt/jws": + let rfc7515_A1_Jwk = initJwk(parseJson(rfc7515_A1_JwkStr)) + let rfc7515_A2_JwkPubKey = initJwk(parseJson(rfc7515_A2_JwkPubStr)) + let rfc7515_A2_JwkPrvKey = initJwk(parseJson(rfc7515_A2_JwkPrvStr)) + let rfc7515_A3_Jwk = initJwk(parseJson(rfc7515_A3_JwkStr)) + let rfc7515_A4_Jwk = initJwk(parseJson(rfc7515_A4_JwkStr)) + let sampleKey = initJwk(parseJson(sampleJwtKey)) + test "HS256 - sign": - check rfc7515A1ExampleJwt == $sign( - header = initJoseHeader(rfc7515A1ExampleHeaderB64), - payload = byteArrToString(rfc7515A1ExampleClaimsBytes), - key = initJwk(parseJson(rfc7515A1ExampleJwkStr))) + check rfc7515_A1_Jwt == $sign( + header = initJoseHeader(rfc7515_A1_HeaderB64), + payload = byteArrToString(rfc7515_A1_ClaimsBytes), + key = rfc7515_A1_Jwk) check sampleJwt == $sign( header = initJoseHeader(parseJson(sampleJwtHeaderDecoded)), @@ -23,27 +30,91 @@ suite "jwt/jws": test "HS256 - verify": validate( - jws = initJWS(rfc7515A1ExampleJwt), + jws = initJWS(rfc7515_A1_Jwt), alg = HS256, - key = initJwk(parseJson(rfc7515A1ExampleJwkStr))) + key = rfc7515_A1_Jwk) validate( jws = initJWS(sampleJwt), alg = HS256, - key = initJwk(parseJson(sampleJwtKey))) + key = sampleKey) test "HS256 - round trip": let payload = "This is a message I want to protect from tampering." - let jwk = initJwk(%*{ - "kty": "oct", - "alg": "HS256", - "k": b64UrlEncode("This is my secret key") - }) let jws = sign( header = initJoseHeader(%*{ "alg": "HS256" }), payload = payload, - key = jwk) + key = sampleKey) - jws.validate(HS256, jwk) + jws.validate(HS256, sampleKey) + + test "RS256 - sign": + + check rfc7515_A2_Jwt == $sign( + header = initJoseHeader(rfc7515_A2_HeaderB64), + payload = b64UrlDecode(rfc7515_A2_ClaimsB64), + key = rfc7515_A2_JwkPrvKey) + + test "RS256 - verify": + + validate( + jws = initJWS(rfc7515_A2_Jwt), + alg = RS256, + key = rfc7515_A2_JwkPubKey) + + test "RS256 - round trip": + + let jws = sign( + header = initJoseHeader(%*{"alg":"RS256"}), + payload = "This is a message I want to protect from tampering.", + key = rfc7515_A2_JwkPrvKey) + + jws.validate(RS256, rfc7515_A2_JwkPubKey) + + test "ES256 - sign": + + check rfc7515_A3_Jwt == $sign( + header = initJoseHeader(rfc7515_A3_HeaderB64), + payload = b64UrlDecode(rfc7515_A3_ClaimsB64), + key = rfc7515_A3_Jwk) + + test "ES256 - verify": + + validate( + jws = initJWS(rfc7515_A3_Jwt), + alg = ES256, + key = rfc7515_A3_Jwk) + + test "ES256 - round trip": + + let jws = sign( + header = initJoseHeader(%*{"alg": "ES256"}), + payload = "This is a message I want to protect from tampering.", + key = rfc7515_A3_Jwk) + + jws.validate(ES256, rfc7515_A3_Jwk) + + test "ES512 - sign": + + check rfc7515_A4_Jwt == $sign( + header = initJoseHeader(rfc7515_A4_HeaderB64), + payload = b64UrlDecode(rfc7515_A4_PayloadB64), + key = rfc7515_A4_Jwk) + + test "ES512 - verify": + + validate( + jws = initJWS(rfc7515_A4_Jwt), + alg = ES512, + key = rfc7515_A4_Jwk) + + test "ES512 - round trip": + + let jws = sign( + header = initJoseHeader(%*{"alg": "ES512"}), + payload = "This is a message I want to protect from tampering.", + key = rfc7515_A4_Jwk) + + jws.validate(ES512, rfc7515_A4_Jwk) diff --git a/src/test/unit/tjwt.nim b/src/test/unit/tjwt.nim new file mode 100644 index 0000000..9b4a5d7 --- /dev/null +++ b/src/test/unit/tjwt.nim @@ -0,0 +1,41 @@ +import std/json, std/unittest + +import jwt/claims, jwt/joseheader, jwt/jwa, jwt/jwk, jwt/jwt +import private/encoding + +import ../testdata + +suite "jwt/jwt": + + let rfc7515_A1_Jwk = initJwk(parseJson(rfc7515_A1_JwkStr)) + let rfc7515_A2_JwkPubKey = initJwk(parseJson(rfc7515_A2_JwkPubStr)) + let rfc7515_A2_JwkPrvKey = initJwk(parseJson(rfc7515_A2_JwkPrvStr)) + let sampleKey = initJwk(parseJson(sampleJwtKey)) + + test "sign - HS256": + check: + sampleJwt == $createSignedJwt( + header = initJoseHeader(b64UrlEncode(sampleJwtHeaderDecoded)), + claims = initJwtClaims(b64UrlEncode(sampleJwtClaimsDecoded)), + sampleKey) + + rfc7515_A1_Jwt == $createSignedJwt( + header = initJoseHeader(rfc7515_A1_HeaderB64), + claims = initJwtClaims(rfc7515_A1_ClaimsB64), + rfc7515_A1_Jwk) + + test "sign - RS256": + check: + rfc7515_A2_Jwt == $createSignedJwt( + header = initJoseHeader(rfc7515_A2_HeaderB64), + claims = initJwtClaims(rfc7515_A2_ClaimsB64), + rfc7515_A2_JwkPrvKey) + + test "validate - HS256": + + initJwt(rfc7515_A1_Jwt).validate(HS256, rfc7515_A1_Jwk) + initJwt(sampleJwt).validate(HS256, sampleKey) + + test "validate - RS256": + + initJwt(rfc7515_A2_Jwt).validate(RS256, rfc7515_A2_JwkPubKey)