Initial JWS implementation.
Supports: * Compact and JSON Serializations * HS256
This commit is contained in:
parent
0c53843e9b
commit
1c886e23b5
283
rfc/rfc7514.txt
283
rfc/rfc7514.txt
@ -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]
|
|
||||||
|
|
@ -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
90
src/main/jwt/claims.nim
Normal 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
110
src/main/jwt/joseheader.nim
Normal 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
31
src/main/jwt/jwa.nim
Normal 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
7
src/main/jwt/jwe.nim
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import ./joseheader
|
||||||
|
|
||||||
|
type
|
||||||
|
JWE* = object
|
||||||
|
header: JoseHeader
|
||||||
|
|
||||||
|
func header*(jwe: JWE): JoseHeader = jwe.header
|
@ -1,10 +1,10 @@
|
|||||||
import std/json, std/options, std/sequtils
|
import std/json, std/options, std/sequtils
|
||||||
|
|
||||||
import ../private/jsonutils
|
import ../private/jsonutils
|
||||||
|
import ./jwa
|
||||||
|
|
||||||
type
|
type
|
||||||
JwkKeyType* = enum EcPublic, EcPrivate, RsaPublic, RsaPrivate, Octet
|
JwkKeyType* = enum EcPublic, EcPrivate, RsaPublic, RsaPrivate, Octet
|
||||||
JwkKty* = enum ktyEC = "EC", ktyRSA = "RSA", ktyOctet = "oct"
|
|
||||||
|
|
||||||
JwkUse* = enum jwkuSignature = "sig", jwkuEncrypt = "enc"
|
JwkUse* = enum jwkuSignature = "sig", jwkuEncrypt = "enc"
|
||||||
|
|
||||||
@ -13,35 +13,6 @@ type
|
|||||||
jwkopWrapKey = "wrapKey", jwkopUnwrapKey = "unwrapKey",
|
jwkopWrapKey = "wrapKey", jwkopUnwrapKey = "unwrapKey",
|
||||||
jwkopDeriveKey = "deriveKey", jwkopDeriveBits = "deriveBits"
|
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
|
EcPubKey = object of RootObj
|
||||||
crv*: string
|
crv*: string
|
||||||
x*: string
|
x*: string
|
||||||
@ -73,27 +44,50 @@ type
|
|||||||
|
|
||||||
JWK* = object
|
JWK* = object
|
||||||
json: JsonNode
|
json: JsonNode
|
||||||
kty*: string
|
kty: string
|
||||||
use*: Option[string]
|
use: Option[string]
|
||||||
key_ops*: Option[string]
|
key_ops: Option[string]
|
||||||
alg*: Option[string]
|
alg: Option[string]
|
||||||
kid*: Option[string]
|
kid: Option[string]
|
||||||
x5u*: Option[string]
|
x5u: Option[string]
|
||||||
x5c*: Option[string]
|
x5c: Option[string]
|
||||||
x5t*: Option[string]
|
x5t: Option[string]
|
||||||
x5tS256*: Option[string]
|
x5tS256: Option[string]
|
||||||
|
|
||||||
case keyKind*: JwkKeyType
|
case keyKind: JwkKeyType
|
||||||
of EcPublic: ecPub*: EcPubKey
|
of EcPublic: ecPub: EcPubKey
|
||||||
of EcPrivate: ecPrv*: EcPrvKey
|
of EcPrivate: ecPrv: EcPrvKey
|
||||||
of RsaPublic: rsaPub*: RsaPubKey
|
of RsaPublic: rsaPub: RsaPubKey
|
||||||
of RsaPrivate: rsaPrv*: RsaPrvKey
|
of RsaPrivate: rsaPrv: RsaPrvKey
|
||||||
of Octet: octKey*: OctetKey
|
of Octet: octKey: OctetKey
|
||||||
|
|
||||||
JwkSet* = seq[JWK]
|
JwkSet* = seq[JWK]
|
||||||
|
|
||||||
func `[]`*(jwk: JWK, key: string): JsonNode = jwk.json[key]
|
## Read-only public accessors to JWK members
|
||||||
func `[]=`*(jwk: JWK, key: string, val: JsonNode): void = jwk.json[key] = val
|
## -----------------------------------------
|
||||||
|
|
||||||
|
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 =
|
func parseEcPubKey*(n: JsonNode): EcPubKey =
|
||||||
EcPubKey(
|
EcPubKey(
|
||||||
@ -135,7 +129,7 @@ func parseRsaPrvKey*(n: JsonNode): RsaPrvKey =
|
|||||||
func parseOctetKey*(n: JsonNode): OctetKey =
|
func parseOctetKey*(n: JsonNode): OctetKey =
|
||||||
OctetKey(k: n.reqStrVal("k"))
|
OctetKey(k: n.reqStrVal("k"))
|
||||||
|
|
||||||
func parseJwk*(n: JsonNode): JWK =
|
func initJwk*(n: JsonNode): JWK =
|
||||||
# TODO: documentation, example, handle encrypted keys (JWEs)
|
# TODO: documentation, example, handle encrypted keys (JWEs)
|
||||||
case n["kty"].getStr(""):
|
case n["kty"].getStr(""):
|
||||||
|
|
||||||
@ -182,10 +176,10 @@ func parseJwk*(n: JsonNode): JWK =
|
|||||||
result.x5t = n.optStrVal("x5t")
|
result.x5t = n.optStrVal("x5t")
|
||||||
result.x5tS256 = n.optStrVal("x5t#S256")
|
result.x5tS256 = n.optStrVal("x5t#S256")
|
||||||
|
|
||||||
func parseJwkSet*(n: JsonNode): JwkSet =
|
func initJwkSet*(n: JsonNode): JwkSet =
|
||||||
# TODO: documentation, examples, handled encrypted set (JWE)
|
# TODO: documentation, examples, handled encrypted set (JWE)
|
||||||
if not n.hasKey("keys") or n["keys"].kind != JArray:
|
if not n.hasKey("keys") or n["keys"].kind != JArray:
|
||||||
raise newException(ValueError, "JWK Set is missing the 'keys' member, " &
|
raise newException(ValueError, "JWK Set is missing the 'keys' member, " &
|
||||||
"or 'keys' is not an array.")
|
"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
215
src/main/jwt/jws.nim
Normal 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
44
src/main/jwt/jwssig.nim
Normal 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
47
src/main/jwt/jwt.nim
Normal 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 =
|
45
src/main/private/crypto.nim
Normal file
45
src/main/private/crypto.nim
Normal 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)
|
39
src/main/private/crypto/hash.nim
Normal file
39
src/main/private/crypto/hash.nim
Normal 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])
|
47
src/main/private/crypto/hmac.nim
Normal file
47
src/main/private/crypto/hmac.nim
Normal 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)
|
23
src/main/private/crypto/rsa.nim
Normal file
23
src/main/private/crypto/rsa.nim
Normal 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
|
||||||
|
""
|
22
src/main/private/encoding.nim
Normal file
22
src/main/private/encoding.nim
Normal 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])
|
@ -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 =
|
func reqStrVal*(n: JsonNode, key: string): string =
|
||||||
if n.hasKey(key):
|
if n.hasKey(key):
|
||||||
@ -10,3 +12,23 @@ func reqStrVal*(n: JsonNode, key: string): string =
|
|||||||
func optStrVal*(n: JsonNode, key: string): Option[string] =
|
func optStrVal*(n: JsonNode, key: string): Option[string] =
|
||||||
if n.hasKey(key): some(n[key].getStr)
|
if n.hasKey(key): some(n[key].getStr)
|
||||||
else: none[string]()
|
else: none[string]()
|
||||||
|
|
||||||
|
func reqIntVal*(n: JsonNode, key: string): BiggestInt =
|
||||||
|
if n.hasKey(key):
|
||||||
|
if n[key].kind == JInt:
|
||||||
|
return n[key].getBiggestInt
|
||||||
|
|
||||||
|
raise newException(ValueError, "value for '" & key & "' is missing or not an integer")
|
||||||
|
|
||||||
|
func optIntVal*(n: JsonNode, key: string): Option[BiggestInt] =
|
||||||
|
if n.hasKey(key) and n[key].kind == JInt:
|
||||||
|
some(n[key].getBiggestInt)
|
||||||
|
else: none[BiggestInt]()
|
||||||
|
|
||||||
|
proc reqNumericDate*(n: Jsonnode, key: string): DateTime =
|
||||||
|
fromNumericDate(n.reqIntVal(key))
|
||||||
|
|
||||||
|
proc optNumericDate*(n: JsonNode, key: string): Option[DateTime] =
|
||||||
|
let val = n.optIntVal(key)
|
||||||
|
if val.isSome: return some(fromNumericDate(val.get))
|
||||||
|
else: return none[DateTime]()
|
||||||
|
5
src/main/private/timeutils.nim
Normal file
5
src/main/private/timeutils.nim
Normal 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
32
src/test/testdata.nim
Normal 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" }"""
|
@ -1,3 +1,7 @@
|
|||||||
import std/unittest
|
import std/unittest
|
||||||
|
|
||||||
|
import ./tclaims
|
||||||
|
import ./tencoding
|
||||||
|
import ./tjoseheader
|
||||||
import ./tjwk
|
import ./tjwk
|
||||||
|
import ./tjws
|
||||||
|
38
src/test/unit/tclaims.nim
Normal file
38
src/test/unit/tclaims.nim
Normal 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
|
35
src/test/unit/tencoding.nim
Normal file
35
src/test/unit/tencoding.nim
Normal 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)
|
18
src/test/unit/tjoseheader.nim
Normal file
18
src/test/unit/tjoseheader.nim
Normal 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"
|
@ -1,6 +1,6 @@
|
|||||||
import std/json, std/options, std/unittest
|
import std/json, std/options, std/unittest
|
||||||
|
|
||||||
import jwt/jwk
|
import jwt/jwa, jwt/jwk
|
||||||
|
|
||||||
suite "jwt/jwk":
|
suite "jwt/jwk":
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ suite "jwt/jwk":
|
|||||||
|
|
||||||
test "parseEcPubKey parses valid keys":
|
test "parseEcPubKey parses valid keys":
|
||||||
let keyNode = rfc7517A1ExamplePubKeysJson["keys"][0]
|
let keyNode = rfc7517A1ExamplePubKeysJson["keys"][0]
|
||||||
let jwk = parseJwk(keyNode)
|
let jwk = initJwk(keyNode)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
jwk.kty == $ktyEC
|
jwk.kty == $ktyEC
|
||||||
@ -72,15 +72,15 @@ suite "jwt/jwk":
|
|||||||
|
|
||||||
let withoutCrv = parseJson($keyNode)
|
let withoutCrv = parseJson($keyNode)
|
||||||
withoutCrv.delete("crv")
|
withoutCrv.delete("crv")
|
||||||
expect ValueError: discard parseJwk(withoutCrv)
|
expect ValueError: discard initJwk(withoutCrv)
|
||||||
|
|
||||||
let withoutX = parseJson($keyNode)
|
let withoutX = parseJson($keyNode)
|
||||||
withoutX.delete("x")
|
withoutX.delete("x")
|
||||||
expect ValueError: discard parseJwk(withoutX)
|
expect ValueError: discard initJwk(withoutX)
|
||||||
|
|
||||||
let withoutY = parseJson($keyNode)
|
let withoutY = parseJson($keyNode)
|
||||||
withoutY.delete("y")
|
withoutY.delete("y")
|
||||||
let jwkWithoutY = parseJwk(withoutY)
|
let jwkWithoutY = initJwk(withoutY)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
jwkWithoutY.kty == $ktyEc
|
jwkWithoutY.kty == $ktyEc
|
||||||
@ -89,7 +89,7 @@ suite "jwt/jwk":
|
|||||||
|
|
||||||
test "parseRsaPubKey parses valid keys":
|
test "parseRsaPubKey parses valid keys":
|
||||||
let keyNode = rfc7517A1ExamplePubKeysJson["keys"][1]
|
let keyNode = rfc7517A1ExamplePubKeysJson["keys"][1]
|
||||||
let jwk = parseJwk(keyNode)
|
let jwk = initJwk(keyNode)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
jwk.kty == $ktyRSA
|
jwk.kty == $ktyRSA
|
||||||
@ -108,15 +108,15 @@ suite "jwt/jwk":
|
|||||||
|
|
||||||
let withoutN = parseJson($keyNode)
|
let withoutN = parseJson($keyNode)
|
||||||
withoutN.delete("n")
|
withoutN.delete("n")
|
||||||
expect ValueError: discard parseJwk(withoutN)
|
expect ValueError: discard initJwk(withoutN)
|
||||||
|
|
||||||
let withoutE = parseJson($keyNode)
|
let withoutE = parseJson($keyNode)
|
||||||
withoutE.delete("e")
|
withoutE.delete("e")
|
||||||
expect ValueError: discard parseJwk(withoutE)
|
expect ValueError: discard initJwk(withoutE)
|
||||||
|
|
||||||
test "parseEcPrvKey parses valid keys":
|
test "parseEcPrvKey parses valid keys":
|
||||||
let keyNode = rfc7517A2ExamplePrvKeysJson["keys"][0]
|
let keyNode = rfc7517A2ExamplePrvKeysJson["keys"][0]
|
||||||
let jwk = parseJwk(keyNode)
|
let jwk = initJwk(keyNode)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
jwk.kty == $ktyEC
|
jwk.kty == $ktyEC
|
||||||
@ -137,15 +137,15 @@ suite "jwt/jwk":
|
|||||||
|
|
||||||
let withoutCrv = parseJson($keyNode)
|
let withoutCrv = parseJson($keyNode)
|
||||||
withoutCrv.delete("crv")
|
withoutCrv.delete("crv")
|
||||||
expect ValueError: discard parseJwk(withoutCrv)
|
expect ValueError: discard initJwk(withoutCrv)
|
||||||
|
|
||||||
let withoutX = parseJson($keyNode)
|
let withoutX = parseJson($keyNode)
|
||||||
withoutX.delete("x")
|
withoutX.delete("x")
|
||||||
expect ValueError: discard parseJwk(withoutX)
|
expect ValueError: discard initJwk(withoutX)
|
||||||
|
|
||||||
let withoutY = parseJson($keyNode)
|
let withoutY = parseJson($keyNode)
|
||||||
withoutY.delete("y")
|
withoutY.delete("y")
|
||||||
let jwkWithoutY = parseJwk(withoutY)
|
let jwkWithoutY = initJwk(withoutY)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
jwkWithoutY.kty == $ktyEc
|
jwkWithoutY.kty == $ktyEc
|
||||||
@ -154,7 +154,7 @@ suite "jwt/jwk":
|
|||||||
|
|
||||||
test "parseRsaPrvKey parses valid keys":
|
test "parseRsaPrvKey parses valid keys":
|
||||||
let keyNode = rfc7517A2ExamplePrvKeysJson["keys"][1]
|
let keyNode = rfc7517A2ExamplePrvKeysJson["keys"][1]
|
||||||
let jwk = parseJwk(keyNode)
|
let jwk = initJwk(keyNode)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
jwk.kty == $ktyRSA
|
jwk.kty == $ktyRSA
|
||||||
@ -185,19 +185,19 @@ suite "jwt/jwk":
|
|||||||
|
|
||||||
let withoutN = parseJson($keyNode)
|
let withoutN = parseJson($keyNode)
|
||||||
withoutN.delete("n")
|
withoutN.delete("n")
|
||||||
expect ValueError: discard parseJwk(withoutN)
|
expect ValueError: discard initJwk(withoutN)
|
||||||
|
|
||||||
let withoutE = parseJson($keyNode)
|
let withoutE = parseJson($keyNode)
|
||||||
withoutE.delete("e")
|
withoutE.delete("e")
|
||||||
expect ValueError: discard parseJwk(withoutE)
|
expect ValueError: discard initJwk(withoutE)
|
||||||
|
|
||||||
let withoutP = parseJson($keyNode)
|
let withoutP = parseJson($keyNode)
|
||||||
withoutP.delete("p")
|
withoutP.delete("p")
|
||||||
let jwkWithoutP = parseJwk(withoutP)
|
let jwkWithoutP = initJwk(withoutP)
|
||||||
check jwkWithoutP.rsaPrv.p.isNone
|
check jwkWithoutP.rsaPrv.p.isNone
|
||||||
|
|
||||||
test "parseJwkSet (public key examples)":
|
test "initJwkSet (public key examples)":
|
||||||
let jwkSet = parseJwkSet(rfc7517A1ExamplePubKeysJson)
|
let jwkSet = initJwkSet(rfc7517A1ExamplePubKeysJson)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
jwkSet.len == 2
|
jwkSet.len == 2
|
||||||
@ -206,8 +206,8 @@ suite "jwt/jwk":
|
|||||||
jwkSet[1].kty == $ktyRSA
|
jwkSet[1].kty == $ktyRSA
|
||||||
jwkSet[1].keyKind == RsaPublic
|
jwkSet[1].keyKind == RsaPublic
|
||||||
|
|
||||||
test "parseJwkSet (private key examples)":
|
test "initJwkSet (private key examples)":
|
||||||
let jwkSet = parseJwkSet(rfc7517A2ExamplePrvKeysJson)
|
let jwkSet = initJwkSet(rfc7517A2ExamplePrvKeysJson)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
jwkSet.len == 2
|
jwkSet.len == 2
|
||||||
|
49
src/test/unit/tjws.nim
Normal file
49
src/test/unit/tjws.nim
Normal 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)
|
Loading…
Reference in New Issue
Block a user