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 ../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
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 =
|
||||
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]()
|
||||
|
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 ./tclaims
|
||||
import ./tencoding
|
||||
import ./tjoseheader
|
||||
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 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
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