Jonathan Bernard a87f92da2d Complete JWS implementation.
Adds support for signature and verification using RS256, RS384, RS512,
ES256, ES384, and ES512.
2021-12-09 22:17:51 -06:00

237 lines
7.1 KiB
Nim

import std/json, std/logging, 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)
debug "Base64url-encoded payload is:\n\t" & parts[1]
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, payloadIsB64Encoded = false): JWS =
## Create a JWS signing the given payload, which can be any arbitrary data.
## The returned JWS can be serialized with the compact serialization. All
## header data will be protected.
validateHeader(header)
let payloadB64 = if payloadIsB64Encoded: payload
else: b64UrlEncode(payload)
let valueToSign = $header & "." & payloadB64
let signatureB64 =
b64UrlEncode(computeSignature(valueToSign, header.alg.get, key))
debug "Signed a value:\n\ttoSign : " & valueToSign &
"\n\tsignature: " & signatureB64
result = JWS(
payloadB64: payloadB64,
signatures: @[
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, payloadIsB64Encoded = true)
proc sign*(
payload: string,
unprotected: JoseHeader,
protected: JoseHeader,
key: JWK,
payloadIsB64Encoded = false): JWS =
## Create a JWS signing the given payload, which can be arbitrary data. The
## returned JWS can be serialized with the compact serialization.
let combinedHeader = combine(unprotected, protected)
validateHeader(combinedHeader)
let payloadB64 = if payloadIsB64Encoded: payload
else: b64UrlEncode(payload)
let valueToSign = $protected & "." & payloadB64
let signatureB64 = b64UrlEncode(
computeSignature(valueToSign, combinedHeader.alg.get, key))
debug "Signed a value:\n\ttoSign : " & valueToSign &
"\n\tsignature: " & signatureB64
result = JWS(
payloadB64: payloadB64,
signatures: @[
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))
debug "Signed a value:\n\ttoSign : " & valueToSign &
"\n\tsignature: " & signatureB64
result = JWS(
payloadB64: jws.payloadB64,
signatures: jws.signatures &
@[initJwsSignature(
protected = protected,
header = unprotected,
signatureB64 = signatureB64)])
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.")
let payload = $(jws[sigIdx].protected) & "." & jws.payloadB64
debug "Verifying a JWS:\n\tpayload : " & payload &
"\n\tsignature: " & jws[sigIdx].signatureB64
if not verifySignature(
payload = payload,
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