Initial JWS implementation.

Supports:
* Compact and JSON Serializations
* HS256
This commit is contained in:
Jonathan Bernard 2021-11-25 08:58:46 -06:00
parent 0c53843e9b
commit 1c886e23b5
24 changed files with 990 additions and 356 deletions

View File

@ -1,283 +0,0 @@
Independent Submission M. Luckie
Request for Comments: 7514 CAIDA
Category: Experimental 1 April 2015
ISSN: 2070-1721
Really Explicit Congestion Notification (RECN)
Abstract
This document proposes a new ICMP message that a router or host may
use to advise a host to reduce the rate at which it sends, in cases
where the host ignores other signals provided by packet loss and
Explicit Congestion Notification (ECN).
Status of This Memo
This document is not an Internet Standards Track specification; it is
published for examination, experimental implementation, and
evaluation.
This document defines an Experimental Protocol for the Internet
community. This is a contribution to the RFC Series, independently
of any other RFC stream. The RFC Editor has chosen to publish this
document at its discretion and makes no statement about its value for
implementation or deployment. Documents approved for publication by
the RFC Editor are not a candidate for any level of Internet
Standard; see Section 2 of RFC 5741.
Information about the current status of this document, any errata,
and how to provide feedback on it may be obtained at
http://www.rfc-editor.org/info/rfc7514.
Copyright Notice
Copyright (c) 2015 IETF Trust and the persons identified as the
document authors. All rights reserved.
This document is subject to BCP 78 and the IETF Trust's Legal
Provisions Relating to IETF Documents
(http://trustee.ietf.org/license-info) in effect on the date of
publication of this document. Please review these documents
carefully, as they describe your rights and restrictions with respect
to this document.
Luckie Experimental [Page 1]
RFC 7514 RECN 1 April 2015
Table of Contents
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1. Requirements Language . . . . . . . . . . . . . . . . . . 2
2. RECN Message Format . . . . . . . . . . . . . . . . . . . . . 2
2.1. Advice to Implementers . . . . . . . . . . . . . . . . . 3
2.2. Relationship to ICMP Source Quench . . . . . . . . . . . 4
3. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 4
4. Security Considerations . . . . . . . . . . . . . . . . . . . 4
5. Normative References . . . . . . . . . . . . . . . . . . . . 4
Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 5
1. Introduction
The deployment of Explicit Congestion Notification (ECN) [RFC3168]
remains stalled. While most operating systems support ECN, it is
currently disabled by default because of fears that enabling ECN will
break transport protocols. This document proposes a new ICMP message
that a router or host may use to advise a host to reduce the rate at
which it sends, in cases where the host ignores other signals such as
packet loss and ECN. We call this message the "Really Explicit
Congestion Notification" (RECN) message because it delivers a less
subtle indication of congestion than packet loss and ECN.
1.1. Requirements Language
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
document are to be interpreted as described in RFC 2119 [RFC2119].
2. RECN Message Format
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Explicit Notification |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| As much of the invoking packet as possible |
+ without the ICMP packet exceeding 576 bytes |
| in IPv4 or the minimum MTU in IPv6 |
Type
IPv4: 4
IPv6: 201
Luckie Experimental [Page 2]
RFC 7514 RECN 1 April 2015
Code
0
Checksum
The checksum is the 16-bit ones's complement of the one's
complement sum of the ICMP message starting with the ICMP type
field. When an RECN message is encapsulated in an IPv6 packet,
the computation includes a "pseudo-header" of IPv6 header fields
as specified in Section 8.1 of [RFC2460]. For computing the
checksum, the checksum field is first set to zero.
Explicit Notification
A 4-byte value that conveys an explicit notification in the ASCII
format defined in [RFC20]. This field MUST NOT be set to zero.
Description
An RECN message SHOULD be sent by a router in response to a host
that is generating traffic at a rate persistently unfair to other
competing flows and that has not reacted to previous packet losses
or ECN marks.
The contents of an RECN message MUST be conveyed to the user
responsible for the traffic. Precisely how this is accomplished
will depend on the capabilities of the host. If text-to-speech
capabilities are available, the contents should be converted to
sound form and audibly rendered. If the system is currently
muted, a pop-up message will suffice.
2.1. Advice to Implementers
As the Explicit Notification field is only 4 bytes, it is not
required that the word be null terminated. A client implementation
should be careful not to use more than those 4 bytes. If a router
chooses a word less than 4 bytes in size, it should null-terminate
that word.
A router should not necessarily send an RECN message every time it
discards a packet due to congestion. Rather, a router should send
these messages whenever it discards a burst of packets from a single
sender. For every packet a router discards in a single burst, it
should send an RECN message. A router may form short sentences
composed of different 4-byte words, and a host should play these
sentences back to the user. A router may escalate the content in the
Explicit Notification field if it determines that a sender has not
Luckie Experimental [Page 3]
RFC 7514 RECN 1 April 2015
adjusted its transmission rate in response to previous RECN messages.
There is no upper bound on the intensity of the escalation, either in
content or sentence length.
2.2. Relationship to ICMP Source Quench
The RECN message was inspired by the ICMP Source Quench message,
which is now deprecated [RFC6633]. Because the RECN message uses a
similar approach, an RECN message uses the same ICMP type when
encapsulated in IPv4 as was used by the ICMP Source Quench message.
3. IANA Considerations
This is an Experimental RFC; the experiment will conclude two years
after the publication of this RFC. During the experiment,
implementers are free to use words of their own choosing (up to four
letters) in RECN messages. If RECN becomes a Standard of any kind, a
list of allowed words will be maintained in an IANA registry. There
are no IANA actions required at this time.
4. Security Considerations
ICMP messages may be used in various attacks [RFC5927]. An attacker
may use the RECN message to cause a host to reduce their transmission
rate for no reason. To prevent such an attack, a host must ensure
the quoted message corresponds to an active flow on the system, and
an attacker MUST set the security flag defined in [RFC3514] to 1 when
the RECN message is carried in an IPv4 packet.
5. Normative References
[RFC20] Cerf, V., "ASCII format for network interchange", STD 80,
RFC 20, October 1969,
<http://www.rfc-editor.org/info/rfc20>.
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
Requirement Levels", BCP 14, RFC 2119, March 1997,
<http://www.rfc-editor.org/info/rfc2119>.
[RFC2460] Deering, S. and R. Hinden, "Internet Protocol, Version 6
(IPv6) Specification", RFC 2460, December 1998,
<http://www.rfc-editor.org/info/rfc2460>.
[RFC3168] Ramakrishnan, K., Floyd, S., and D. Black, "The Addition
of Explicit Congestion Notification (ECN) to IP", RFC
3168, September 2001,
<http://www.rfc-editor.org/info/rfc3168>.
Luckie Experimental [Page 4]
RFC 7514 RECN 1 April 2015
[RFC3514] Bellovin, S., "The Security Flag in the IPv4 Header", RFC
3514, April 2003,
<http://www.rfc-editor.org/info/rfc3514>.
[RFC5927] Gont, F., "ICMP Attacks against TCP", RFC 5927, July 2010,
<http://www.rfc-editor.org/info/rfc5927>.
[RFC6633] Gont, F., "Deprecation of ICMP Source Quench Messages",
RFC 6633, May 2012,
<http://www.rfc-editor.org/info/rfc6633>.
Author's Address
Matthew Luckie
CAIDA
University of California, San Diego
9500 Gilman Drive
La Jolla, CA 92093-0505
United States
EMail: mjl@caida.org
Luckie Experimental [Page 5]

View File

@ -1,3 +1,3 @@
import jwt/jwk
import jwt/jwa, jwt/jwe, jwt/jwk, jwt/jws, jwt/jwt
export jwk
export jwa, jwe, jwk, jws, jwt

90
src/main/jwt/claims.nim Normal file
View File

@ -0,0 +1,90 @@
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 = 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)

110
src/main/jwt/joseheader.nim Normal file
View File

@ -0,0 +1,110 @@
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)

31
src/main/jwt/jwa.nim Normal file
View File

@ -0,0 +1,31 @@
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"
EcCrv* = enum P256 = "P-256", P384 = "P-384", P521 = "P-521"

7
src/main/jwt/jwe.nim Normal file
View File

@ -0,0 +1,7 @@
import ./joseheader
type
JWE* = object
header: JoseHeader
func header*(jwe: JWE): JoseHeader = jwe.header

View File

@ -1,10 +1,10 @@
import std/json, std/options, std/sequtils
import ../private/jsonutils
import ./jwa
type
JwkKeyType* = enum EcPublic, EcPrivate, RsaPublic, RsaPrivate, Octet
JwkKty* = enum ktyEC = "EC", ktyRSA = "RSA", ktyOctet = "oct"
JwkUse* = enum jwkuSignature = "sig", jwkuEncrypt = "enc"
@ -13,35 +13,6 @@ type
jwkopWrapKey = "wrapKey", jwkopUnwrapKey = "unwrapKey",
jwkopDeriveKey = "deriveKey", jwkopDeriveBits = "deriveBits"
JwkAlg* = 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"
EcCrv* = enum P256 = "P-256", P384 = "P-384", P521 = "P-521"
EcPubKey = object of RootObj
crv*: string
x*: string
@ -73,27 +44,50 @@ type
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]
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
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]
func `[]`*(jwk: JWK, key: string): JsonNode = jwk.json[key]
func `[]=`*(jwk: JWK, key: string, val: JsonNode): void = jwk.json[key] = val
## 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(
@ -135,7 +129,7 @@ func parseRsaPrvKey*(n: JsonNode): RsaPrvKey =
func parseOctetKey*(n: JsonNode): OctetKey =
OctetKey(k: n.reqStrVal("k"))
func parseJwk*(n: JsonNode): JWK =
func initJwk*(n: JsonNode): JWK =
# TODO: documentation, example, handle encrypted keys (JWEs)
case n["kty"].getStr(""):
@ -182,10 +176,10 @@ func parseJwk*(n: JsonNode): JWK =
result.x5t = n.optStrVal("x5t")
result.x5tS256 = n.optStrVal("x5t#S256")
func parseJwkSet*(n: JsonNode): JwkSet =
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(parseJwk(it))
return n["keys"].getElems.mapIt(initJwk(it))

215
src/main/jwt/jws.nim Normal file
View File

@ -0,0 +1,215 @@
import std/json, 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)
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): 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 valueToSign = $header & "." & payloadB64
let signatureB64 =
b64UrlEncode(computeSignature(valueToSign, header.alg.get, key))
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)
proc sign*(
payload: string,
unprotected: JoseHeader,
protected: JoseHeader,
key: JWK): 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 valueToSign = $protected & "." & payloadB64
let signatureB64 = b64UrlEncode(
computeSignature(valueToSign, combinedHeader.alg.get, key))
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): 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))
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, 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.")
if not verifySignature(
payload = $(jws[sigIdx].protected) & "." & jws.payloadB64,
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): bool =
try:
jws.validate(alg, key)
return true
except: return false

44
src/main/jwt/jwssig.nim Normal file
View File

@ -0,0 +1,44 @@
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
}

47
src/main/jwt/jwt.nim Normal file
View File

@ -0,0 +1,47 @@
import std/json, std/options, std/strutils, std/times
import ./claims, ./joseheader, ./jwe, ./jws
export claims
type
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.header
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
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]))
proc initJWT*(header: JoseHeader, claims: JwtClaims): JWT =
result = JWT(
header: header,
claims: claims,
signature: none[string]())
# proc verify*(jwt: JWT): bool =
# proc sign*(jwt: JWT, key: JWK): JWT =

View File

@ -0,0 +1,45 @@
import std/options
import bearssl
import ../jwt/jwa
import ../jwt/jwk
import ./crypto/hash
import ./crypto/hmac
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 =
validateAlgMatchesKey(alg, key)
case alg:
of HS256, HS384, HS512: return hmac(alg, key, payload)
else:
raise newException(Exception,
"unsupported signature algorithm: " & $alg)
proc verifySignature*(
payload: string,
signature: string,
alg: JwtAlgorithm,
key: JWK
): bool =
validateAlgMatchesKey(alg, key)
case alg:
of HS256, HS384, HS512:
let hash = hmac(alg, key, payload)
return hash == signature
else:
raise newException(Exception,
"unsupported signature algorithm: " & $alg)

View File

@ -0,0 +1,39 @@
import std/tables
import bearssl
type
HashAlgorithm* = enum SHA1, SHA256, SHA384, SHA512
HashCfg* = object
vtable*: ptr HashClass
oid*: cstring
size*: int
let HASHES* = newTable[HashAlgorithm, HashCfg]([
(SHA1, HashCfg(
vtable: addr sha1Vtable,
oid: HASH_OID_SHA1,
size: sha1Size)),
(SHA256, HashCfg(
vtable: addr sha256Vtable,
oid: HASH_OID_SHA256,
size: sha256Size)),
(SHA384, HashCfg(
vtable: addr sha384Vtable,
oid: HASH_OID_SHA384,
size: sha384Size)),
(SHA512, HashCfg(
vtable: addr sha512Vtable,
oid: HASH_OID_SHA512,
size: sha512Size)),
])
proc hash*(alg: HashAlgorithm, data: string): 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.output(pCtx, addr result[0])

View File

@ -0,0 +1,47 @@
import std/logging
import bearssl
import ../../jwt/jwa
import ../../jwt/jwk
import ../encoding
proc bearHMAC(alg: JwtAlgorithm, key, toSign: 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, toSign.cstring, toSign.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*(alg: JwtAlgorithm, key: JWK, toSign: string): 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)

View File

@ -0,0 +1,23 @@
import bearssl
import ../../jwt/jwa
import ../../jwt/jwk
proc bearRsaSign(
alg: JwtAlgorithm,
key: RsaPrivateKey,
toSign: string
): string =
var hashVtable: ptr HashClass
var hashOID: cstring
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
else: raise newException(ValueError,
"Unsupported RSA signature algorithm '" & $alg & "'")
# TODO
""

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

View File

@ -1,4 +1,6 @@
import std/json, std/options, std/strutils
import std/json, std/options, std/strutils, std/times
import ./timeutils
func reqStrVal*(n: JsonNode, key: string): string =
if n.hasKey(key):
@ -10,3 +12,23 @@ func reqStrVal*(n: JsonNode, key: string): string =
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]()

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)

32
src/test/testdata.nim Normal file
View File

@ -0,0 +1,32 @@
## 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]
## 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* = """{
"kty":"oct",
"k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}"""
const rfc7515A1ExampleJwt* = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
# 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 sampleJwt* = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYmMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.fKGoqlw-Nporec-dVTrNbC0ZC7kAhUsEs50s12Iwy14"
const sampleJwtHeaderDecoded* = """{"alg":"HS256","typ":"JWT"}"""
const sampleJwtClaimsDecoded* = """{"sub":"abc","name":"John Doe","iat":1516239022}"""
const sampleJwtKey* = """{ "kty": "oct", "k": "eW91ci0yNTYtYml0LXNlY3JldAo" }"""

View File

@ -1,3 +1,7 @@
import std/unittest
import ./tclaims
import ./tencoding
import ./tjoseheader
import ./tjwk
import ./tjws

38
src/test/unit/tclaims.nim Normal file
View File

@ -0,0 +1,38 @@
import std/json, std/options, std/strutils, std/unittest
import jwt/claims
import ../testdata
suite "jwt/claims":
test "initClaims(b64)":
let rfcClaims = initJwtClaims(rfc7519Sec3_1ExampleClaimsB64)
let sampleClaims = initJwtClaims(sampleJwt.split('.')[1])
check:
rfcClaims.iss.isSome
rfcClaims.iss.get == "joe"
rfcClaims.exp.isSome
rfcClaims["http://example.com/is_root"].isSome
rfcClaims["http://example.com/is_root"].get.kind == JBool
rfcClaims["http://example.com/is_root"].get.getBool == true
sampleClaims.sub.isSome
sampleClaims.sub.get == "abc"
sampleClaims.iat.isSome
sampleClaims["name"].isSome
sampleClaims["name"].get.getStr == "John Doe"
test "round trip":
let rfcClaims1 = initJwtClaims(rfc7519Sec3_1ExampleClaimsB64)
let rfcClaims2 = initJwtClaims($rfcClaims1)
let sampleClaims1 = initJwtClaims(sampleJwt.split('.')[1])
let sampleClaims2 = initJwtClaims($sampleClaims1)
check:
rfcClaims1.iss == rfcClaims2.iss
rfcClaims1.exp == rfcClaims2.exp
rfcClaims1.exp == rfcClaims2.exp
sampleClaims1.sub == sampleClaims2.sub
sampleClaims1.iat == sampleClaims2.iat
sampleClaims1["name"].get.getStr == sampleClaims2["name"].get.getStr

View File

@ -0,0 +1,35 @@
import std/json, std/options, std/strutils, std/unittest
import private/encoding
import ../testdata
suite "private/encoding":
test "b64UrlEncode(int array)":
check:
rfc7519Sec3_1ExampleHeaderB64 == b64UrlEncode(rfc7519Sec3_1ExampleHeaderBytes)
rfc7519Sec3_1ExampleClaimsB64 == b64UrlEncode(rfc7519Sec3_1ExampleClaimsBytes)
test "b64UrlEncode(string)":
let jwtParts = sampleJwt.split('.')
check:
b64UrlEncode(sampleJwtHeaderDecoded) == jwtParts[0]
b64UrlEncode(sampleJwtClaimsDecoded) == jwtParts[1]
# 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)
test "b64UrlDecode(string)":
let jwtParts = sampleJwt.split('.')
check:
sampleJwtHeaderDecoded == b64UrlDecode(jwtParts[0])
sampleJwtClaimsDecoded == b64UrlDecode(jwtParts[1])
test "byteArrToString":
check:
byteArrToString(rfc7515A1ExampleHeaderBytes) == b64UrlDecode(rfc7515A1ExampleHeaderB64)

View File

@ -0,0 +1,18 @@
import std/json, std/options, std/unittest
import jwt/jwa
import jwt/joseheader
import ../testdata
suite "jwt/joseheader":
test "initJoseHeader always sets rawB64":
let h1 = initJoseHeader(rfc7519Sec3_1ExampleHeaderB64)
let h2 = initJoseHeader(parseJson(rfc7519Sec3_1ExampleHeaderStr))
let h3 = initJoseHeader(alg = some(HS256), typ = some("JWT"))
check:
h1.rawB64 == rfc7519Sec3_1ExampleHeaderB64
h2.rawB64 == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"
h3.rawB64 == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"

View File

@ -1,6 +1,6 @@
import std/json, std/options, std/unittest
import jwt/jwk
import jwt/jwa, jwt/jwk
suite "jwt/jwk":
@ -52,7 +52,7 @@ suite "jwt/jwk":
test "parseEcPubKey parses valid keys":
let keyNode = rfc7517A1ExamplePubKeysJson["keys"][0]
let jwk = parseJwk(keyNode)
let jwk = initJwk(keyNode)
check:
jwk.kty == $ktyEC
@ -72,15 +72,15 @@ suite "jwt/jwk":
let withoutCrv = parseJson($keyNode)
withoutCrv.delete("crv")
expect ValueError: discard parseJwk(withoutCrv)
expect ValueError: discard initJwk(withoutCrv)
let withoutX = parseJson($keyNode)
withoutX.delete("x")
expect ValueError: discard parseJwk(withoutX)
expect ValueError: discard initJwk(withoutX)
let withoutY = parseJson($keyNode)
withoutY.delete("y")
let jwkWithoutY = parseJwk(withoutY)
let jwkWithoutY = initJwk(withoutY)
check:
jwkWithoutY.kty == $ktyEc
@ -89,7 +89,7 @@ suite "jwt/jwk":
test "parseRsaPubKey parses valid keys":
let keyNode = rfc7517A1ExamplePubKeysJson["keys"][1]
let jwk = parseJwk(keyNode)
let jwk = initJwk(keyNode)
check:
jwk.kty == $ktyRSA
@ -108,15 +108,15 @@ suite "jwt/jwk":
let withoutN = parseJson($keyNode)
withoutN.delete("n")
expect ValueError: discard parseJwk(withoutN)
expect ValueError: discard initJwk(withoutN)
let withoutE = parseJson($keyNode)
withoutE.delete("e")
expect ValueError: discard parseJwk(withoutE)
expect ValueError: discard initJwk(withoutE)
test "parseEcPrvKey parses valid keys":
let keyNode = rfc7517A2ExamplePrvKeysJson["keys"][0]
let jwk = parseJwk(keyNode)
let jwk = initJwk(keyNode)
check:
jwk.kty == $ktyEC
@ -137,15 +137,15 @@ suite "jwt/jwk":
let withoutCrv = parseJson($keyNode)
withoutCrv.delete("crv")
expect ValueError: discard parseJwk(withoutCrv)
expect ValueError: discard initJwk(withoutCrv)
let withoutX = parseJson($keyNode)
withoutX.delete("x")
expect ValueError: discard parseJwk(withoutX)
expect ValueError: discard initJwk(withoutX)
let withoutY = parseJson($keyNode)
withoutY.delete("y")
let jwkWithoutY = parseJwk(withoutY)
let jwkWithoutY = initJwk(withoutY)
check:
jwkWithoutY.kty == $ktyEc
@ -154,7 +154,7 @@ suite "jwt/jwk":
test "parseRsaPrvKey parses valid keys":
let keyNode = rfc7517A2ExamplePrvKeysJson["keys"][1]
let jwk = parseJwk(keyNode)
let jwk = initJwk(keyNode)
check:
jwk.kty == $ktyRSA
@ -185,19 +185,19 @@ suite "jwt/jwk":
let withoutN = parseJson($keyNode)
withoutN.delete("n")
expect ValueError: discard parseJwk(withoutN)
expect ValueError: discard initJwk(withoutN)
let withoutE = parseJson($keyNode)
withoutE.delete("e")
expect ValueError: discard parseJwk(withoutE)
expect ValueError: discard initJwk(withoutE)
let withoutP = parseJson($keyNode)
withoutP.delete("p")
let jwkWithoutP = parseJwk(withoutP)
let jwkWithoutP = initJwk(withoutP)
check jwkWithoutP.rsaPrv.p.isNone
test "parseJwkSet (public key examples)":
let jwkSet = parseJwkSet(rfc7517A1ExamplePubKeysJson)
test "initJwkSet (public key examples)":
let jwkSet = initJwkSet(rfc7517A1ExamplePubKeysJson)
check:
jwkSet.len == 2
@ -206,8 +206,8 @@ suite "jwt/jwk":
jwkSet[1].kty == $ktyRSA
jwkSet[1].keyKind == RsaPublic
test "parseJwkSet (private key examples)":
let jwkSet = parseJwkSet(rfc7517A2ExamplePrvKeysJson)
test "initJwkSet (private key examples)":
let jwkSet = initJwkSet(rfc7517A2ExamplePrvKeysJson)
check:
jwkSet.len == 2

49
src/test/unit/tjws.nim Normal file
View File

@ -0,0 +1,49 @@
import std/json, std/unittest
import jwt/claims, jwt/joseheader, jwt/jwa, jwt/jwk, jwt/jws, jwt/jwssig
import private/encoding
import ../testdata
suite "jwt/jws":
test "HS256 - sign":
check rfc7515A1ExampleJwt == $sign(
header = initJoseHeader(rfc7515A1ExampleHeaderB64),
payload = byteArrToString(rfc7515A1ExampleClaimsBytes),
key = initJwk(parseJson(rfc7515A1ExampleJwkStr)))
check sampleJwt == $sign(
header = initJoseHeader(parseJson(sampleJwtHeaderDecoded)),
payload = $initJwtClaims(parseJson(sampleJwtClaimsDecoded)),
key = initJwk(parseJson(sampleJwtKey)))
test "HS256 - verify":
validate(
jws = initJWS(rfc7515A1ExampleJwt),
alg = HS256,
key = initJwk(parseJson(rfc7515A1ExampleJwkStr)))
validate(
jws = initJWS(sampleJwt),
alg = HS256,
key = initJwk(parseJson(sampleJwtKey)))
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)
jws.validate(HS256, jwk)