diff --git a/rfc/rfc7514.txt b/rfc/rfc7514.txt deleted file mode 100644 index 0e31607..0000000 --- a/rfc/rfc7514.txt +++ /dev/null @@ -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, - . - - [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate - Requirement Levels", BCP 14, RFC 2119, March 1997, - . - - [RFC2460] Deering, S. and R. Hinden, "Internet Protocol, Version 6 - (IPv6) Specification", RFC 2460, December 1998, - . - - [RFC3168] Ramakrishnan, K., Floyd, S., and D. Black, "The Addition - of Explicit Congestion Notification (ECN) to IP", RFC - 3168, September 2001, - . - - - - -Luckie Experimental [Page 4] - -RFC 7514 RECN 1 April 2015 - - - [RFC3514] Bellovin, S., "The Security Flag in the IPv4 Header", RFC - 3514, April 2003, - . - - [RFC5927] Gont, F., "ICMP Attacks against TCP", RFC 5927, July 2010, - . - - [RFC6633] Gont, F., "Deprecation of ICMP Source Quench Messages", - RFC 6633, May 2012, - . - -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] - diff --git a/src/main/jwt.nim b/src/main/jwt.nim index c1a6b01..438eaf0 100644 --- a/src/main/jwt.nim +++ b/src/main/jwt.nim @@ -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 diff --git a/src/main/jwt/claims.nim b/src/main/jwt/claims.nim new file mode 100644 index 0000000..402c819 --- /dev/null +++ b/src/main/jwt/claims.nim @@ -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) diff --git a/src/main/jwt/joseheader.nim b/src/main/jwt/joseheader.nim new file mode 100644 index 0000000..274cc31 --- /dev/null +++ b/src/main/jwt/joseheader.nim @@ -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) diff --git a/src/main/jwt/jwa.nim b/src/main/jwt/jwa.nim new file mode 100644 index 0000000..4d199f1 --- /dev/null +++ b/src/main/jwt/jwa.nim @@ -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" diff --git a/src/main/jwt/jwe.nim b/src/main/jwt/jwe.nim new file mode 100644 index 0000000..82f5ae7 --- /dev/null +++ b/src/main/jwt/jwe.nim @@ -0,0 +1,7 @@ +import ./joseheader + +type + JWE* = object + header: JoseHeader + +func header*(jwe: JWE): JoseHeader = jwe.header diff --git a/src/main/jwt/jwk.nim b/src/main/jwt/jwk.nim index 8136261..2db24e6 100644 --- a/src/main/jwt/jwk.nim +++ b/src/main/jwt/jwk.nim @@ -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)) diff --git a/src/main/jwt/jws.nim b/src/main/jwt/jws.nim new file mode 100644 index 0000000..185ca11 --- /dev/null +++ b/src/main/jwt/jws.nim @@ -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 diff --git a/src/main/jwt/jwssig.nim b/src/main/jwt/jwssig.nim new file mode 100644 index 0000000..311d543 --- /dev/null +++ b/src/main/jwt/jwssig.nim @@ -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 + } diff --git a/src/main/jwt/jwt.nim b/src/main/jwt/jwt.nim new file mode 100644 index 0000000..baf87a6 --- /dev/null +++ b/src/main/jwt/jwt.nim @@ -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 = diff --git a/src/main/private/crypto.nim b/src/main/private/crypto.nim new file mode 100644 index 0000000..14b1220 --- /dev/null +++ b/src/main/private/crypto.nim @@ -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) diff --git a/src/main/private/crypto/hash.nim b/src/main/private/crypto/hash.nim new file mode 100644 index 0000000..affd5b5 --- /dev/null +++ b/src/main/private/crypto/hash.nim @@ -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]) diff --git a/src/main/private/crypto/hmac.nim b/src/main/private/crypto/hmac.nim new file mode 100644 index 0000000..d0189ed --- /dev/null +++ b/src/main/private/crypto/hmac.nim @@ -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) diff --git a/src/main/private/crypto/rsa.nim b/src/main/private/crypto/rsa.nim new file mode 100644 index 0000000..fe718b8 --- /dev/null +++ b/src/main/private/crypto/rsa.nim @@ -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 + "" diff --git a/src/main/private/encoding.nim b/src/main/private/encoding.nim new file mode 100644 index 0000000..8666423 --- /dev/null +++ b/src/main/private/encoding.nim @@ -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.. 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) diff --git a/src/test/unit/tjoseheader.nim b/src/test/unit/tjoseheader.nim new file mode 100644 index 0000000..527e5c8 --- /dev/null +++ b/src/test/unit/tjoseheader.nim @@ -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" diff --git a/src/test/unit/tjwk.nim b/src/test/unit/tjwk.nim index 76e6f5b..9622c69 100644 --- a/src/test/unit/tjwk.nim +++ b/src/test/unit/tjwk.nim @@ -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 diff --git a/src/test/unit/tjws.nim b/src/test/unit/tjws.nim new file mode 100644 index 0000000..99c65af --- /dev/null +++ b/src/test/unit/tjws.nim @@ -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)