Merge pull request #329 from benmcollins/315-rfc7797-b64
jwt: RFC 7797 unencoded (b64=false) + detached payloads
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e86e7aa..96417bd 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -415,6 +415,9 @@
# JWS JSON Serialization (multi-signature)
list (APPEND UNIT_TESTS jws_json)
+ # RFC 7797 unencoded / detached payload
+ list (APPEND UNIT_TESTS jws_b64)
+
# ML-DSA (FIPS 204 / RFC 9964). The test is a no-op unless the build
# enabled WITH_ML_DSA against a capable backend.
list (APPEND UNIT_TESTS jwt_mldsa)
diff --git a/README.md b/README.md
index 38e0cde..f366b40 100644
--- a/README.md
+++ b/README.md
@@ -99,6 +99,18 @@
every compatible key, always under the usual algorithm/key-type binding.
`jwt_checker_verify()` auto-detects Compact vs JSON input.
+##### Unencoded and detached payloads (RFC 7797)
+
+For a generic JWS over an opaque (non-claims) payload — e.g. HTTP message
+signatures or JAdES/eIDAS detached signatures — set the payload bytes with
+`jwt_builder_setpayload()`. `jwt_builder_setb64(b, 0)` selects the
+[RFC 7797](https://datatracker.ietf.org/doc/html/rfc7797) unencoded form
+(`"b64":false`, with `"b64"` marked critical and the signature computed over the
+raw payload), and `jwt_builder_set_detached()` omits the payload from the output.
+A detached token is verified with `jwt_checker_verify_detached()`, supplying the
+payload out-of-band. As mandated by RFC 7797 §6, the checker rejects a
+`"b64":false` token unless `"b64"` appears in `"crit"`.
+
#### JWE
LibJWT supports JWE (RFC 7516) in both the Compact Serialization and the JSON
diff --git a/include/jwt.h b/include/jwt.h
index f7dce39..52c577d 100644
--- a/include/jwt.h
+++ b/include/jwt.h
@@ -606,6 +606,59 @@
const char *key, const char *value_json);
/**
+ * @brief Sign an opaque (non-claims) payload
+ *
+ * Sets a raw payload to be signed verbatim instead of JSON claims, for a
+ * generic JWS (RFC 7515) — for example RFC 7797 unencoded payloads or a
+ * detached signature over an arbitrary document (JAdES). This is mutually
+ * exclusive with claims: when a raw payload is set it is used and any claims
+ * are ignored. Pass @p data as NULL (or @p len 0) to clear it and return to
+ * the claims model. The bytes are copied.
+ *
+ * @param builder Pointer to a builder object
+ * @param data The payload bytes (copied), or NULL to clear
+ * @param len The payload length in bytes
+ * @return 0 on success, non-zero otherwise with error set in the builder
+ * @since 3.6.0
+ */
+JWT_EXPORT
+int jwt_builder_setpayload(jwt_builder_t *builder, const unsigned char *data,
+ size_t len);
+
+/**
+ * @brief Select base64url or unencoded payload (RFC 7797)
+ *
+ * With @p b64 nonzero (the default) the payload is base64url-encoded as usual.
+ * With @p b64 zero, @rfc{7797} unencoded payloads are produced: ``"b64":false``
+ * is emitted in the protected header, ``"b64"`` is added to ``"crit"``, and the
+ * signature is computed over the **raw** payload bytes. Requires a raw payload
+ * (jwt_builder_setpayload()), since RFC 7797 forbids the option for JWTs.
+ *
+ * @param builder Pointer to a builder object
+ * @param b64 Nonzero for base64url (default), zero for an unencoded payload
+ * @return 0 on success, non-zero otherwise with error set in the builder
+ * @since 3.6.0
+ */
+JWT_EXPORT
+int jwt_builder_setb64(jwt_builder_t *builder, int b64);
+
+/**
+ * @brief Detach the payload from the output
+ *
+ * With @p detached nonzero, the produced token omits the payload (an empty
+ * payload part in the Compact form, no ``"payload"`` member in the JSON forms).
+ * The verifier must supply the payload out-of-band with
+ * jwt_checker_verify_detached(). Orthogonal to jwt_builder_setb64().
+ *
+ * @param builder Pointer to a builder object
+ * @param detached Nonzero to omit the payload from the output
+ * @return 0 on success, non-zero otherwise with error set in the builder
+ * @since 3.6.0
+ */
+JWT_EXPORT
+int jwt_builder_set_detached(jwt_builder_t *builder, int detached);
+
+/**
* @brief Set IssuedAt usage on builder
*
* By default, the builder will set the ``iat`` claim to all tokens. You can
@@ -1077,6 +1130,27 @@
unsigned int index);
/**
+ * @brief Verify a token whose payload was detached
+ *
+ * Verifies a token produced with jwt_builder_set_detached() (or any JWS with a
+ * detached payload), supplying the payload out-of-band. Works for both
+ * base64url and @rfc{7797} unencoded (``"b64":false``) payloads; the signing
+ * input is reconstructed from @p payload according to the token's ``"b64"``
+ * header. As with jwt_checker_verify(), an unencoded payload is accepted only
+ * if ``"b64"`` is present in the token's ``"crit"`` header (RFC 7797 §6).
+ *
+ * @param checker Pointer to a checker object
+ * @param token A string containing the (detached) token to verify
+ * @param payload The out-of-band payload bytes
+ * @param len The payload length in bytes
+ * @return 0 on success, non-zero otherwise with error set in the checker
+ * @since 3.6.0
+ */
+JWT_EXPORT
+int jwt_checker_verify_detached(jwt_checker_t *checker, const char *token,
+ const unsigned char *payload, size_t len);
+
+/**
* @}
* @noop jwt_checker_grp
*/
diff --git a/libjwt/jwt-common.c b/libjwt/jwt-common.c
index 7990085..ee33ce4 100644
--- a/libjwt/jwt-common.c
+++ b/libjwt/jwt-common.c
@@ -52,6 +52,9 @@
}
}
+ /* @rfc{7797} Free any raw payload (jwt_builder_setpayload()). */
+ jwt_scrub_and_free(__cmd->c.payload_raw, __cmd->c.payload_raw_len);
+
memset(__cmd, 0, sizeof(*__cmd));
jwt_freemem(__cmd);
@@ -70,6 +73,9 @@
* (builder) or the JSON parse (checker) materializes it. */
INIT_LIST_HEAD(&__cmd->c.signatures);
+ /* @rfc{7797} base64url payloads by default (b64=true). */
+ __cmd->c.b64 = 1;
+
__cmd->c.payload = jwt_json_create();
__cmd->c.headers = jwt_json_create();
__cmd->c.claims = CLAIMS_DEF;
@@ -715,6 +721,51 @@
return 0;
}
+int jwt_builder_setpayload(jwt_builder_t *builder, const unsigned char *data,
+ size_t len)
+{
+ unsigned char *copy = NULL;
+
+ if (builder == NULL)
+ return 1;
+
+ if (data != NULL && len > 0) {
+ copy = jwt_malloc(len);
+ if (copy == NULL) {
+ jwt_write_error(builder, "Error allocating memory"); // LCOV_EXCL_LINE
+ return 1; // LCOV_EXCL_LINE
+ }
+ memcpy(copy, data, len);
+ }
+
+ /* Replace any previous raw payload (NULL/0 clears it). */
+ jwt_scrub_and_free(builder->c.payload_raw, builder->c.payload_raw_len);
+ builder->c.payload_raw = copy;
+ builder->c.payload_raw_len = copy ? len : 0;
+
+ return 0;
+}
+
+int jwt_builder_setb64(jwt_builder_t *builder, int b64)
+{
+ if (builder == NULL)
+ return 1;
+
+ builder->c.b64 = b64 ? 1 : 0;
+
+ return 0;
+}
+
+int jwt_builder_set_detached(jwt_builder_t *builder, int detached)
+{
+ if (builder == NULL)
+ return 1;
+
+ builder->c.detached = detached ? 1 : 0;
+
+ return 0;
+}
+
jwt_signature_t *jwt_builder_add_signature(jwt_builder_t *builder,
jwt_alg_t alg, const jwk_item_t *key)
{
@@ -831,6 +882,37 @@
jwt->alg = config.alg;
jwt->key = config.key;
+ /* @rfc{7797} Thread the unencoded/detached options onto the token. */
+ jwt->b64 = __cmd->c.b64;
+ jwt->detached = __cmd->c.detached;
+ jwt->payload_raw = __cmd->c.payload_raw;
+ jwt->payload_raw_len = __cmd->c.payload_raw_len;
+
+ if (!jwt->b64) {
+ /* RFC 7797 forbids b64=false for JWTs (JSON claim sets), so it
+ * is only valid over a raw payload. */
+ if (jwt->payload_raw == NULL) {
+ jwt_write_error(__cmd,
+ "b64=false (RFC 7797) requires a raw payload "
+ "set with jwt_builder_setpayload()");
+ return NULL;
+ }
+ /* An unencoded payload appears verbatim in the serialized token
+ * (a C string / JSON string), which cannot carry an embedded NUL.
+ * Reject it rather than silently truncate. */
+ if (memchr(jwt->payload_raw, '\0', jwt->payload_raw_len) != NULL) {
+ jwt_write_error(__cmd,
+ "An unencoded (b64=false) payload must not "
+ "contain a NUL byte");
+ return NULL;
+ }
+ /* @rfc{7797,6} Emit "b64":false and mark "b64" critical. */
+ if (jwt_apply_b64_header(jwt)) {
+ jwt_copy_error(__cmd, jwt);
+ return NULL;
+ }
+ }
+
/* @rfc{7515,4.1.11} Emit the "crit" header if any were registered.
* Done after the callback so it can add the headers being marked. */
if (jwt_write_crit(jwt, __cmd->c.understood)) {
diff --git a/libjwt/jwt-encode.c b/libjwt/jwt-encode.c
index 0167ef5..6c496bd 100644
--- a/libjwt/jwt-encode.c
+++ b/libjwt/jwt-encode.c
@@ -216,17 +216,13 @@
{
char_auto *payload = NULL;
char *buf = NULL;
- int payload_len;
+ size_t payload_len;
struct jwt_signature *s;
jwt_json_auto_t *out_obj = NULL;
jwt_json_t *sig_arr = NULL, *v;
- /* Shared payload, encoded once. */
- if (write_js(jwt->claims, &buf))
- return NULL; // LCOV_EXCL_LINE
- payload_len = jwt_base64uri_encode(&payload, buf, (int)strlen(buf));
- jwt_freemem(buf);
- if (payload_len <= 0) {
+ /* @rfc{7797} Shared payload (base64url or raw), encoded once. */
+ if (jwt_build_payload_part(jwt, &payload, &payload_len)) {
jwt_write_error(jwt, "Error encoding payload"); // LCOV_EXCL_LINE
return NULL; // LCOV_EXCL_LINE
}
@@ -235,9 +231,12 @@
if (out_obj == NULL)
return NULL; // LCOV_EXCL_LINE
- v = jwt_json_create_str(payload);
- if (v == NULL || jwt_json_obj_set(out_obj, "payload", v))
- return NULL; // LCOV_EXCL_LINE
+ /* @rfc{7515,7.2.1} Detached: omit "payload" (supplied out-of-band). */
+ if (!jwt->detached) {
+ v = jwt_json_create_str(payload);
+ if (v == NULL || jwt_json_obj_set(out_obj, "payload", v))
+ return NULL; // LCOV_EXCL_LINE
+ }
if (cmd->format == JWT_FORMAT_JSON_GENERAL) {
sig_arr = jwt_json_create_arr();
@@ -251,6 +250,7 @@
char_auto *prot_b64 = NULL, *sig_b64 = NULL, *input = NULL;
char *rawsig = NULL;
unsigned int rawsig_len = 0;
+ size_t input_len;
int prot_len, enc;
jwt_alg_t alg;
@@ -282,16 +282,23 @@
jwt_write_error(jwt, "Error encoding protected header"); // LCOV_EXCL_LINE
return NULL; // LCOV_EXCL_LINE
}
+ /* The return counts stripped '=' padding; use the true length. */
+ prot_len = (int)strlen(prot_b64);
- /* Signing input: BASE64URL(protected) "." BASE64URL(payload). */
- input = jwt_malloc((size_t)prot_len + payload_len + 2);
+ /* @rfc{7797,3} Signing input: BASE64URL(protected) "." payload
+ * (payload is base64url or raw per b64; binary-safe). */
+ input_len = (size_t)prot_len + 1 + payload_len;
+ input = jwt_malloc(input_len + 1);
if (input == NULL)
return NULL; // LCOV_EXCL_LINE
- sprintf(input, "%s.%s", prot_b64, payload);
+ memcpy(input, prot_b64, prot_len);
+ input[prot_len] = '.';
+ memcpy(input + prot_len + 1, payload, payload_len);
+ input[input_len] = '\0';
jwt->alg = alg;
jwt->key = s->key;
- if (jwt_sign(jwt, &rawsig, &rawsig_len, input, strlen(input)))
+ if (jwt_sign(jwt, &rawsig, &rawsig_len, input, input_len))
return NULL;
enc = jwt_base64uri_encode(&sig_b64, rawsig, (int)rawsig_len);
@@ -510,11 +517,96 @@
return 0;
}
+/* @rfc{7797} Build the payload as it appears after the first '.': the raw
+ * payload bytes (jwt_builder_setpayload()) or the serialized claims, then
+ * base64url-encoded unless b64 is false. Returns a malloc'd, NUL-terminated
+ * buffer (binary-safe via @out_len). 0 on success. */
+int jwt_build_payload_part(jwt_t *jwt, char **out, size_t *out_len)
+{
+ const unsigned char *bytes;
+ char_auto *claims_str = NULL;
+ size_t len;
+
+ if (jwt->payload_raw != NULL) {
+ bytes = jwt->payload_raw;
+ len = jwt->payload_raw_len;
+ } else {
+ if (write_js(jwt->claims, &claims_str))
+ return 1; // LCOV_EXCL_LINE
+ bytes = (const unsigned char *)claims_str;
+ len = strlen(claims_str);
+ }
+
+ if (jwt->b64) {
+ int n;
+
+ if (len > INT_MAX)
+ return 1; // LCOV_EXCL_LINE
+ n = jwt_base64uri_encode(out, (const char *)bytes, (int)len);
+ if (n <= 0)
+ return 1; // LCOV_EXCL_LINE
+ /* The return counts stripped '=' padding; the string is shorter. */
+ *out_len = strlen(*out);
+ } else {
+ char *copy = jwt_malloc(len + 1);
+
+ if (copy == NULL)
+ return 1; // LCOV_EXCL_LINE
+ memcpy(copy, bytes, len);
+ copy[len] = '\0';
+ *out = copy;
+ *out_len = len;
+ }
+
+ return 0;
+}
+
+/* @rfc{7797,6} For an unencoded payload, the protected header must carry
+ * "b64":false and "b64" MUST be marked critical. Inject both into @jwt->headers
+ * (the shared base, which the compact path signs directly and the JSON path
+ * clones into each signature). */
+int jwt_apply_b64_header(jwt_t *jwt)
+{
+ jwt_json_t *crit, *ent, *v;
+ size_t i;
+ int found = 0;
+
+ v = jwt_json_create_bool(0);
+ if (v == NULL || jwt_json_obj_set(jwt->headers, "b64", v))
+ return 1; // LCOV_EXCL_LINE
+
+ crit = jwt_json_obj_get(jwt->headers, "crit");
+ if (crit == NULL) {
+ crit = jwt_json_create_arr();
+ if (crit == NULL || jwt_json_obj_set(jwt->headers, "crit", crit))
+ return 1; // LCOV_EXCL_LINE
+ } else if (!jwt_json_is_array(crit)) {
+ jwt_write_error(jwt, "\"crit\" header must be an array");
+ return 1;
+ }
+
+ jwt_json_arr_foreach(crit, i, ent) {
+ if (jwt_json_is_string(ent) &&
+ !strcmp(jwt_json_str_val(ent), "b64")) {
+ found = 1;
+ break;
+ }
+ }
+ if (!found) {
+ v = jwt_json_create_str("b64");
+ if (v == NULL || jwt_json_arr_append(crit, v))
+ return 1; // LCOV_EXCL_LINE
+ }
+
+ return 0;
+}
+
static int jwt_encode(jwt_t *jwt, char **out)
{
- char_auto *head = NULL, *payload = NULL, *sig = NULL;
- char *buf = NULL;
- int ret, head_len, payload_len;
+ char_auto *head = NULL, *payload = NULL, *sig_b64 = NULL;
+ char *buf = NULL, *si = NULL, *token = NULL, *p;
+ int head_len, ret;
+ size_t payload_len, si_len, pout_len, token_len, sb_len;
unsigned int sig_len;
if (out == NULL) {
@@ -525,110 +617,95 @@
}
*out = NULL;
- /* First the header. */
- ret = write_js(jwt->headers, &buf);
- if (ret)
+ /* Header. */
+ if (write_js(jwt->headers, &buf))
return 1; // LCOV_EXCL_LINE
- /* Encode it */
head_len = jwt_base64uri_encode(&head, buf, (int)strlen(buf));
jwt_freemem(buf);
-
if (head_len <= 0) {
// LCOV_EXCL_START
jwt_write_error(jwt, "Error encoding header");
return 1;
// LCOV_EXCL_STOP
}
+ /* The return counts stripped '=' padding; use the true string length. */
+ head_len = (int)strlen(head);
- /* Now the payload. */
- ret = write_js(jwt->claims, &buf);
- if (ret) {
- // LCOV_EXCL_START
- jwt_write_error(jwt, "Error writing payload");
- return 1;
- // LCOV_EXCL_STOP
+ /* @rfc{7797} Payload part (base64url or raw). */
+ if (jwt_build_payload_part(jwt, &payload, &payload_len)) {
+ jwt_write_error(jwt, "Error encoding payload"); // LCOV_EXCL_LINE
+ return 1; // LCOV_EXCL_LINE
}
- payload_len = jwt_base64uri_encode(&payload, buf, (int)strlen(buf));
- jwt_freemem(buf);
-
- if (payload_len <= 0) {
- // LCOV_EXCL_START
- jwt_write_error(jwt, "Error encoding payload");
- return 1;
- // LCOV_EXCL_STOP
+ if ((size_t)head_len > SIZE_MAX - payload_len - 2) {
+ jwt_write_error(jwt, "Encoded token too large"); // LCOV_EXCL_LINE
+ return 1; // LCOV_EXCL_LINE
}
- /* head_len and payload_len are each bounded by jwt_base64uri_encode to
- * encode within INT_MAX, but guard their sum so head_len + payload_len + 3
- * cannot overflow the int allocation size below. */
- if (head_len > INT_MAX - payload_len - 3) {
- // LCOV_EXCL_START
- jwt_write_error(jwt, "Encoded token too large");
- return 1;
- // LCOV_EXCL_STOP
+ /* Signing input: BASE64URL(header) "." payload (binary-safe). */
+ si_len = (size_t)head_len + 1 + payload_len;
+ si = jwt_malloc(si_len + 1);
+ if (si == NULL) {
+ jwt_write_error(jwt, "Error allocating memory"); // LCOV_EXCL_LINE
+ return 1; // LCOV_EXCL_LINE
}
+ memcpy(si, head, head_len);
+ si[head_len] = '.';
+ memcpy(si + head_len + 1, payload, payload_len);
+ si[si_len] = '\0';
- /* The part we need to sign, but add space for 2 dots and a nil */
- buf = jwt_malloc(head_len + payload_len + 3);
- if (buf == NULL) {
- // LCOV_EXCL_START
- jwt_write_error(jwt, "Error allocating memory");
- return 1;
- // LCOV_EXCL_STOP
- }
-
- strcpy(buf, head);
- strcat(buf, ".");
- strcat(buf, payload);
-
+ /* Signature (empty for an unsecured "none" token). */
if (jwt->alg == JWT_ALG_NONE) {
- /* Add the trailing dot, and send it back */
- strcat(buf, ".");
- *out = buf;
- return 0;
- }
-
- /* At this point buf has "head.payload" */
-
- /* Now the signature. */
- ret = jwt_sign(jwt, &sig, &sig_len, buf, strlen(buf));
- jwt_freemem(buf);
- if (ret) {
- // LCOV_EXCL_START
- jwt_write_error(jwt, "Error allocating memory");
- return ret;
- // LCOV_EXCL_STOP
- }
-
- ret = jwt_base64uri_encode(&buf, sig, sig_len);
- /* At this point buf has b64 of sig and ret is size of it */
-
- if (ret < 0) {
- // LCOV_EXCL_START
- jwt_write_error(jwt, "Error allocating memory");
- return 1;
- // LCOV_EXCL_STOP
- }
-
- /* plus 2 dots and a nil */
- ret = strlen(head) + strlen(payload) + strlen(buf) + 3;
-
- /* We're good, so let's get it all together */
- *out = jwt_malloc(ret);
- // LCOV_EXCL_START
- if (*out == NULL) {
- jwt_write_error(jwt, "Error allocating memory");
- ret = 1;
+ sig_b64 = jwt_malloc(1);
+ if (sig_b64 != NULL)
+ sig_b64[0] = '\0';
} else {
- sprintf(*out, "%s.%s.%s", head, payload, buf);
- ret = 0;
+ char *rawsig = NULL;
+
+ ret = jwt_sign(jwt, &rawsig, &sig_len, si, si_len);
+ if (ret) {
+ jwt_freemem(si);
+ return ret;
+ }
+ ret = jwt_base64uri_encode(&sig_b64, rawsig, sig_len);
+ jwt_freemem(rawsig);
+ if (ret < 0) {
+ jwt_write_error(jwt, "Error encoding signature"); // LCOV_EXCL_LINE
+ jwt_freemem(si); // LCOV_EXCL_LINE
+ return 1; // LCOV_EXCL_LINE
+ }
}
- // LCOV_EXCL_STOP
+ jwt_freemem(si);
- jwt_freemem(buf);
+ if (sig_b64 == NULL) {
+ jwt_write_error(jwt, "Error allocating memory"); // LCOV_EXCL_LINE
+ return 1; // LCOV_EXCL_LINE
+ }
- return ret;
+ /* @rfc{7797} Assemble: header "." (detached ? "" : payload) "." sig. */
+ sb_len = strlen(sig_b64);
+ pout_len = jwt->detached ? 0 : payload_len;
+ token_len = (size_t)head_len + 1 + pout_len + 1 + sb_len; token = jwt_malloc(token_len + 1);
+ if (token == NULL) {
+ jwt_write_error(jwt, "Error allocating memory"); // LCOV_EXCL_LINE
+ return 1; // LCOV_EXCL_LINE
+ }
+ p = token;
+ memcpy(p, head, head_len);
+ p += head_len;
+ *p++ = '.';
+ if (pout_len) {
+ memcpy(p, payload, pout_len);
+ p += pout_len;
+ }
+ *p++ = '.';
+ memcpy(p, sig_b64, sb_len);
+ p += sb_len;
+ *p = '\0';
+
+ *out = token;
+
+ return 0;
}
char *jwt_encode_str(jwt_t *jwt)
diff --git a/libjwt/jwt-private.h b/libjwt/jwt-private.h
index 26435bf..b7e2242 100644
--- a/libjwt/jwt-private.h
+++ b/libjwt/jwt-private.h
@@ -112,6 +112,16 @@
const jwk_set_t *keyring;
jwt_verify_policy_t policy;
unsigned int last_sig_count; /* signatures in the last JSON token */
+
+ /* --- @rfc{7797} Unencoded payload / detached payload ---
+ * An opaque payload set via jwt_builder_setpayload() (mutually exclusive
+ * with JSON claims); signed as-is. @b64 (default 1) selects base64url vs
+ * the RFC 7797 unencoded ("b64":false) signing input; @detached omits the
+ * payload from the output (supplied out-of-band on verify). */
+ unsigned char *payload_raw;
+ size_t payload_raw_len;
+ int b64;
+ int detached;
};
struct jwt_builder {
@@ -235,6 +245,15 @@
jwt_alg_t alg;
int error;
char error_msg[JWT_ERR_LEN];
+
+ /* @rfc{7797} An opaque payload (instead of JSON @claims), and the b64 /
+ * detached flags, threaded from the builder/checker into encode/verify.
+ * @payload_raw is borrowed (owned by jwt_common or the caller). */
+ const unsigned char *payload_raw;
+ size_t payload_raw_len;
+ int b64;
+ int detached;
+
union {
struct jwt_checker *checker;
struct jwt_builder *builder;
@@ -574,6 +593,17 @@
JWT_NO_EXPORT
char *jwt_encode_json(jwt_t *jwt, struct jwt_common *cmd);
+/* @rfc{7797} The payload as it appears after the first '.': raw bytes or
+ * serialized claims, base64url-encoded unless jwt->b64 is false. Malloc'd,
+ * NUL-terminated, binary-safe via @out_len. 0 on success. */
+JWT_NO_EXPORT
+int jwt_build_payload_part(jwt_t *jwt, char **out, size_t *out_len);
+
+/* @rfc{7797,6} Inject "b64":false + "b64" in "crit" into jwt->headers (for an
+ * unencoded payload). 0 on success. */
+JWT_NO_EXPORT
+int jwt_apply_b64_header(jwt_t *jwt);
+
/* Checker: parse + verify a JWS JSON Serialization against the checker's
* key/keyring and policy. Returns 0 if the policy is satisfied. */
JWT_NO_EXPORT
diff --git a/libjwt/jwt-verify.c b/libjwt/jwt-verify.c
index b47b5cb..a02a25a 100644
--- a/libjwt/jwt-verify.c
+++ b/libjwt/jwt-verify.c
@@ -135,8 +135,13 @@
return 1;
}
+ /* @rfc{7797} The library handles "b64" itself, so it is always
+ * understood (and is enforced separately during parsing). */
+ if (!strcmp(name, "b64"))
+ found = 1;
+
/* Must be understood by the application. */
- if (understood) {
+ if (!found && understood) {
size_t j;
for (j = 0; understood[j] != NULL; j++) {
@@ -157,54 +162,123 @@
return 0;
}
+/* @rfc{7797,6} Read the "b64" header (default true). A false value is the
+ * RFC 7797 unencoded payload option and is ONLY accepted if "b64" is also
+ * present in "crit" (else an attacker could flip the signing-input meaning).
+ * Sets *b64; returns 1 on the crit violation or a non-boolean "b64". */
+static int jwt_payload_b64(jwt_t *jwt, int *b64)
+{
+ jwt_json_t *jb64, *crit, *ent;
+ size_t i;
+ int found = 0;
+
+ *b64 = 1;
+
+ jb64 = jwt_json_obj_get(jwt->headers, "b64");
+ if (jb64 == NULL)
+ return 0;
+
+ if (!jwt_json_is_bool(jb64)) {
+ jwt_write_error(jwt, "\"b64\" header must be a boolean");
+ return 1;
+ }
+
+ if (jwt_json_is_true(jb64))
+ return 0;
+
+ *b64 = 0;
+
+ crit = jwt_json_obj_get(jwt->headers, "crit");
+ if (crit != NULL && jwt_json_is_array(crit)) {
+ jwt_json_arr_foreach(crit, i, ent) {
+ if (jwt_json_is_string(ent) &&
+ !strcmp(jwt_json_str_val(ent), "b64")) {
+ found = 1;
+ break;
+ }
+ }
+ }
+ if (!found) {
+ jwt_write_error(jwt,
+ "\"b64\":false requires \"b64\" in \"crit\" (RFC 7797)");
+ return 1;
+ }
+
+ return 0;
+}
+
int jwt_parse(jwt_t *jwt, const char *token, unsigned int *len)
{
- char_auto *head = NULL;
- char *payload, *sig;
- int head_len = strlen(token) + 1;
+ char_auto *buf = NULL;
+ char *payload, *sig, *dot, *lastdot = NULL;
+ int buf_len = strlen(token) + 1;
+ int b64;
- head = jwt_malloc(head_len);
- if (!head) {
+ buf = jwt_malloc(buf_len);
+ if (!buf) {
// LCOV_EXCL_START
jwt_write_error(jwt, "Error allocating memory");
return 1;
// LCOV_EXCL_STOP
}
- /* head_len includes nil */
- memcpy(head, token, head_len);
+ /* buf_len includes nil */
+ memcpy(buf, token, buf_len);
- /* Find the components. */
- for (payload = head; payload[0] != '.'; payload++) {
+ /* Header: everything up to the first '.'. */
+ for (payload = buf; payload[0] != '.'; payload++) {
if (payload[0] == '\0') {
jwt_write_error(jwt,
"No dot found looking for end of header");
return 1;
}
}
-
payload[0] = '\0';
payload++;
- for (sig = payload; sig[0] != '.'; sig++) {
- if (sig[0] == '\0') {
+ /* Parse the header now so we know the "b64" setting. */
+ if (jwt_parse_head(jwt, buf))
+ return 1;
+
+ /* @rfc{7797} Determine the payload encoding (and enforce crit). */
+ if (jwt_payload_b64(jwt, &b64))
+ return 1;
+ jwt->b64 = b64;
+
+ if (b64) {
+ /* Standard: the payload is between the 1st and 2nd '.'. */
+ for (sig = payload; sig[0] != '.'; sig++) {
+ if (sig[0] == '\0') {
+ jwt_write_error(jwt,
+ "No dot found looking for end of payload");
+ return 1;
+ }
+ }
+ sig[0] = '\0';
+ sig++; /* point past the dot, as the b64=false branch does */
+
+ if (jwt_parse_payload(jwt, payload))
+ return 1;
+ } else {
+ /* @rfc{7797,5.2} Unencoded: the signature is after the LAST '.'
+ * and the raw payload (which may itself contain '.') is between
+ * the 1st and last '.'. The raw payload is not JSON claims. */
+ for (dot = payload; dot[0] != '\0'; dot++) {
+ if (dot[0] == '.')
+ lastdot = dot;
+ }
+ if (lastdot == NULL) {
jwt_write_error(jwt,
- "No dot found looking for end of payload");
+ "No dot found looking for signature");
return 1;
}
+ *lastdot = '\0';
+ sig = lastdot + 1;
}
- sig[0] = '\0';
-
- /* Now that we have everything split up, let's check out the
- * header. */
- if (jwt_parse_head(jwt, head))
- return 1;
-
- if (jwt_parse_payload(jwt, payload))
- return 1;
-
- *len = sig - head;
+ /* @rfc{7515,5.1}/@rfc{7797,3} The signing input is buf[0 .. sig's dot],
+ * i.e. BASE64URL(header) "." (base64url or raw) payload. */
+ *len = (sig - 1) - buf;
return 0;
}
@@ -349,8 +423,9 @@
static int __verify_config_post(jwt_t *jwt, const jwt_config_t *config,
unsigned int sig_len)
{
- /* Yes, we do this before checking a signature. */
- if (__verify_claims(jwt)) {
+ /* Yes, we do this before checking a signature. @rfc{7797} An unencoded
+ * (b64=false) payload is opaque, not JSON claims, so skip claim checks. */
+ if (jwt->b64 && __verify_claims(jwt)) {
/* TODO Pass back the ORd list of claims failed. */
jwt_write_error(jwt, "Failed one or more claims");
return 1;
@@ -609,6 +684,17 @@
return 1;
}
+ /* @rfc{7797,6} Enforce "b64" in "crit" if this signature is unencoded. */
+ {
+ int eb64;
+
+ if (jwt_payload_b64(jwt, &eb64)) {
+ jwt_copy_error(checker, jwt);
+ jwt->headers = NULL;
+ return 1;
+ }
+ }
+
/* Seed the candidate: a kid-named keyring key (a binding assertion, no
* fallback), or the checker's single key. A keyless keyring entry scans. */
kid = json_str(s->protected, "kid");
@@ -657,7 +743,7 @@
jwt_auto_t *jwt = NULL;
const char *payload_b64;
struct jwt_signature *s;
- int n_verified = 0, sig_ok, payload_len;
+ int n_verified = 0, sig_ok, payload_len, pb64 = 1;
size_t n_entries;
/* Reset for a reused checker. */
@@ -720,6 +806,18 @@
return 1;
}
+ /* @rfc{7797} The shared payload's encoding comes from the (shared)
+ * signatures; read it from the first one. An unencoded payload is opaque
+ * (not JSON claims), so it is neither base64-decoded nor claim-checked. */
+ {
+ struct jwt_signature *first = jwt_signature_first(&checker->c);
+ jwt_json_t *jb = first ?
+ jwt_json_obj_get(first->protected, "b64") : NULL;
+
+ if (jb != NULL && jwt_json_is_bool(jb) && !jwt_json_is_true(jb))
+ pb64 = 0;
+ }
+
/* A transient JWT carrying the shared payload for the per-signature
* verify and the read-only claim checks. */
jwt = jwt_new();
@@ -728,14 +826,21 @@
return 1; // LCOV_EXCL_LINE
}
jwt->checker = checker;
+ jwt->b64 = pb64;
jwt_json_releasep(&jwt->claims);
/* Release the empty header object jwt_new() allocated: from here on
* jwt->headers only ever borrows each signature's protected header. */
jwt_json_releasep(&jwt->headers);
- jwt->claims = jwt_base64uri_decode_to_json((char *)payload_b64);
- if (jwt->claims == NULL) {
- jwt_write_error(checker, "Error parsing payload");
- return 1;
+ if (pb64) {
+ jwt->claims = jwt_base64uri_decode_to_json((char *)payload_b64);
+ if (jwt->claims == NULL) {
+ jwt_write_error(checker, "Error parsing payload");
+ return 1;
+ }
+ } else {
+ jwt->claims = jwt_json_create();
+ if (jwt->claims == NULL)
+ return 1; // LCOV_EXCL_LINE
}
list_for_each_entry(s, &checker->c.signatures, node) {
@@ -759,7 +864,7 @@
jwt_write_error(checker,
"Signature policy not met (%d of %zu verified)",
n_verified, n_entries);
- } else if (__verify_claims(jwt)) {
+ } else if (jwt->b64 && __verify_claims(jwt)) {
jwt_write_error(checker, "Failed one or more claims");
} else if (__verify_jti(jwt)) {
/* jti runs only after signature + claims succeed. */
@@ -768,3 +873,151 @@
return checker->error;
}
+
+/* Read the "b64" flag from a base64url-encoded protected header. Default 1. */
+static int detached_b64(const char *prot_b64)
+{
+ jwt_json_auto_t *hdr = NULL;
+ char_auto *copy = NULL;
+ jwt_json_t *jb;
+
+ if (prot_b64 == NULL)
+ return 1;
+ copy = jwt_str_dup(prot_b64);
+ if (copy == NULL)
+ return 1; // LCOV_EXCL_LINE
+ hdr = jwt_base64uri_decode_to_json(copy);
+ if (hdr == NULL)
+ return 1;
+ jb = jwt_json_obj_get(hdr, "b64");
+ if (jb != NULL && jwt_json_is_bool(jb) && !jwt_json_is_true(jb))
+ return 0;
+
+ return 1;
+}
+
+/* @rfc{7797,3} The payload as it appears in the signing input: base64url(@payload)
+ * when b64, else the raw bytes. Returns a malloc'd NUL-terminated string. */
+static char *detached_payload_part(int b64, const unsigned char *payload,
+ size_t len)
+{
+ char *pp = NULL;
+
+ if (b64) {
+ if (len == 0)
+ return jwt_str_dup("");
+ if (len > INT_MAX ||
+ jwt_base64uri_encode(&pp, (const char *)payload, (int)len) <= 0)
+ return NULL; // LCOV_EXCL_LINE
+ return pp;
+ }
+
+ pp = jwt_malloc(len + 1);
+ if (pp == NULL)
+ return NULL; // LCOV_EXCL_LINE
+ if (len)
+ memcpy(pp, payload, len);
+ pp[len] = '\0';
+
+ return pp;
+}
+
+int jwt_checker_verify_detached(jwt_checker_t *checker, const char *token,
+ const unsigned char *payload, size_t len)
+{
+ char_auto *pp = NULL, *recon = NULL;
+ const char *p;
+ int b64, ret;
+
+ if (checker == NULL)
+ return 1;
+
+ if (token == NULL || !strlen(token)) {
+ jwt_write_error(checker, "Must pass a token");
+ return 1;
+ }
+ if (payload == NULL && len > 0) {
+ jwt_write_error(checker, "Must pass a payload");
+ return 1;
+ }
+
+ p = token;
+ while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
+ p++;
+
+ if (*p == '{') {
+ /* @rfc{7515,7.2} JSON: insert the out-of-band "payload" and verify
+ * the now-attached token. */
+ jwt_json_auto_t *root = NULL;
+ jwt_json_t *sigs, *prot_j, *v;
+ const char *prot_b64;
+
+ root = jwt_json_parse(token, JWT_JSON_REJECT_DUPLICATES, NULL);
+ if (root == NULL || !jwt_json_is_object(root)) {
+ jwt_write_error(checker, "Invalid JWS JSON Serialization");
+ return 1;
+ }
+
+ sigs = jwt_json_obj_get(root, "signatures");
+ if (sigs != NULL && jwt_json_is_array(sigs) &&
+ jwt_json_arr_size(sigs) > 0)
+ prot_j = jwt_json_obj_get(jwt_json_arr_get(sigs, 0),
+ "protected");
+ else
+ prot_j = jwt_json_obj_get(root, "protected");
+ prot_b64 = (prot_j && jwt_json_is_string(prot_j))
+ ? jwt_json_str_val(prot_j) : NULL;
+
+ b64 = detached_b64(prot_b64);
+ pp = detached_payload_part(b64, payload, len);
+ if (pp == NULL) {
+ jwt_write_error(checker, "Error allocating memory"); // LCOV_EXCL_LINE
+ return 1; // LCOV_EXCL_LINE
+ }
+
+ v = jwt_json_create_str(pp);
+ if (v == NULL || jwt_json_obj_set(root, "payload", v))
+ return 1; // LCOV_EXCL_LINE
+
+ recon = jwt_json_serialize(root, JWT_JSON_COMPACT);
+ if (recon == NULL)
+ return 1; // LCOV_EXCL_LINE
+ } else {
+ /* @rfc{7515,7.1} Compact: rebuild header "." payload "." signature. */
+ const char *firstdot = strchr(token, '.');
+ const char *lastdot = strrchr(token, '.');
+ char_auto *head_b64 = NULL;
+ size_t head_len, sig_len, recon_len;
+ const char *sig;
+
+ if (firstdot == NULL || lastdot == firstdot) {
+ jwt_write_error(checker,
+ "Not a detached Compact Serialization");
+ return 1;
+ }
+ head_len = (size_t)(firstdot - token);
+ sig = lastdot + 1;
+ sig_len = strlen(sig);
+
+ head_b64 = jwt_malloc(head_len + 1);
+ if (head_b64 == NULL)
+ return 1; // LCOV_EXCL_LINE
+ memcpy(head_b64, token, head_len);
+ head_b64[head_len] = '\0';
+
+ b64 = detached_b64(head_b64);
+ pp = detached_payload_part(b64, payload, len);
+ if (pp == NULL)
+ return 1; // LCOV_EXCL_LINE
+
+ recon_len = head_len + 1 + strlen(pp) + 1 + sig_len;
+ recon = jwt_malloc(recon_len + 1);
+ if (recon == NULL)
+ return 1; // LCOV_EXCL_LINE
+ sprintf(recon, "%s.%s.%s", head_b64, pp, sig);
+ }
+
+ ret = jwt_checker_verify(checker, recon);
+
+ return ret;
+}
diff --git a/tests/jws_b64.c b/tests/jws_b64.c
new file mode 100644
index 0000000..b09d9be
--- /dev/null
+++ b/tests/jws_b64.c
@@ -0,0 +1,234 @@
+/* Public domain, no copyright. Use at your own risk. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "jwt_tests.h"
+
+/* @rfc{7797} JWS unencoded ("b64":false) + detached payloads (issue #315).
+ * Runs under each compiled crypto backend (SET_OPS loop) and both JSON
+ * backends. The Appendix A payload "$.02" is non-JSON on purpose. */
+
+static const unsigned char RAW[] = "$.02";
+#define RAW_LEN 4
+
+static jwk_set_t *load_one(const char *file)
+{
+ char *path;
+ jwk_set_t *set;
+ int ret;
+
+ ret = asprintf(&path, KEYDIR "/%s", file);
+ ck_assert_int_gt(ret, 0);
+ set = jwks_create_fromfile(path);
+ free(path);
+ ck_assert_ptr_nonnull(set);
+
+ return set;
+}
+
+static const jwt_serialization_t formats[] = {
+ JWT_FORMAT_COMPACT, JWT_FORMAT_JSON_FLAT, JWT_FORMAT_JSON_GENERAL,
+};
+
+/* ---- b64=false, attached, every serialization ---- */
+START_TEST(test_b64_false_attached)
+{
+ jwk_set_t *ks;
+ const jwk_item_t *ec;
+ size_t f;
+
+ SET_OPS();
+ ks = load_one("ec_key_prime256v1.json");
+ ec = jwks_item_get(ks, 0);
+
+ for (f = 0; f < ARRAY_SIZE(formats); f++) {
+ jwt_builder_auto_t *b = jwt_builder_new();
+ jwt_checker_auto_t *c = jwt_checker_new();
+ char_auto *tok = NULL;
+
+ ck_assert_int_eq(jwt_builder_setkey(b, JWT_ALG_ES256, ec), 0);
+ ck_assert_int_eq(jwt_builder_setpayload(b, RAW, RAW_LEN), 0);
+ ck_assert_int_eq(jwt_builder_setb64(b, 0), 0);
+ ck_assert_int_eq(jwt_builder_set_format(b, formats[f]), 0);
+ tok = jwt_builder_generate(b);
+ ck_assert_ptr_nonnull(tok);
+
+ ck_assert_int_eq(jwt_checker_setkey(c, JWT_ALG_ES256, ec), 0);
+ ck_assert_int_eq(jwt_checker_verify(c, tok), 0);
+ }
+
+ jwks_free(ks);
+}
+END_TEST
+
+/* ---- b64=false, detached, every serialization ---- */
+START_TEST(test_b64_false_detached)
+{
+ jwk_set_t *ks;
+ const jwk_item_t *ec;
+ size_t f;
+
+ SET_OPS();
+ ks = load_one("ec_key_prime256v1.json");
+ ec = jwks_item_get(ks, 0);
+
+ for (f = 0; f < ARRAY_SIZE(formats); f++) {
+ jwt_builder_auto_t *b = jwt_builder_new();
+ jwt_checker_auto_t *c = jwt_checker_new();
+ char_auto *tok = NULL;
+
+ ck_assert_int_eq(jwt_builder_setkey(b, JWT_ALG_ES256, ec), 0);
+ ck_assert_int_eq(jwt_builder_setpayload(b, RAW, RAW_LEN), 0);
+ ck_assert_int_eq(jwt_builder_setb64(b, 0), 0);
+ ck_assert_int_eq(jwt_builder_set_detached(b, 1), 0);
+ ck_assert_int_eq(jwt_builder_set_format(b, formats[f]), 0);
+ tok = jwt_builder_generate(b);
+ ck_assert_ptr_nonnull(tok);
+
+ ck_assert_int_eq(jwt_checker_setkey(c, JWT_ALG_ES256, ec), 0);
+ /* Correct out-of-band payload verifies; a wrong one is rejected. */
+ ck_assert_int_eq(jwt_checker_verify_detached(c, tok, RAW, RAW_LEN), 0);
+ ck_assert_int_ne(jwt_checker_verify_detached(c, tok,
+ (const unsigned char *)"x.99", 4), 0);
+ }
+
+ jwks_free(ks);
+}
+END_TEST
+
+/* ---- b64=true detached JSON claims (a detached JWT, RFC 7515 App F) ---- */
+START_TEST(test_b64_true_detached_jwt)
+{
+ static const unsigned char JSON[] = "{\"sub\":\"alice\"}";
+ jwk_set_t *ks;
+ const jwk_item_t *ec;
+ jwt_builder_auto_t *b = NULL;
+ jwt_checker_auto_t *c = NULL;
+ char_auto *tok = NULL;
+
+ SET_OPS();
+ ks = load_one("ec_key_prime256v1.json");
+ ec = jwks_item_get(ks, 0);
+
+ b = jwt_builder_new();
+ ck_assert_int_eq(jwt_builder_setkey(b, JWT_ALG_ES256, ec), 0);
+ ck_assert_int_eq(jwt_builder_setpayload(b, JSON, strlen((char *)JSON)), 0);
+ ck_assert_int_eq(jwt_builder_set_detached(b, 1), 0);
+ tok = jwt_builder_generate(b);
+ ck_assert_ptr_nonnull(tok);
+
+ c = jwt_checker_new();
+ ck_assert_int_eq(jwt_checker_setkey(c, JWT_ALG_ES256, ec), 0);
+ ck_assert_int_eq(jwt_checker_verify_detached(c, tok, JSON,
+ strlen((char *)JSON)), 0);
+
+ jwks_free(ks);
+}
+END_TEST
+
+/* ---- RFC 7797 Appendix A.4: HS256, detached, unencoded "$.02" ---- */
+START_TEST(test_rfc7797_appendix_a)
+{
+ /* The HMAC key from RFC 7515 Appendix A.1. */
+ static const char *KEY =
+ "{\"kty\":\"oct\",\"k\":\"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ"
+ "-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow\"}";
+ /* The detached JWS from RFC 7797 Appendix A.4. */
+ static const char *TOKEN =
+ "eyJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19"
+ "..A5dxf2s96_n5FLueVuW1Z_vh161FwXZC4YLPff6dmDY";
+ jwk_set_t *ks;
+ const jwk_item_t *k;
+ jwt_checker_auto_t *c = NULL, *c2 = NULL;
+
+ SET_OPS();
+
+ ks = jwks_create(NULL);
+ ck_assert_ptr_nonnull(ks);
+ ck_assert_ptr_nonnull(jwks_load(ks, KEY));
+ k = jwks_item_get(ks, 0);
+ ck_assert_ptr_nonnull(k);
+
+ c = jwt_checker_new();
+ ck_assert_int_eq(jwt_checker_setkey(c, JWT_ALG_HS256, k), 0);
+ ck_assert_int_eq(jwt_checker_verify_detached(c, TOKEN, RAW, RAW_LEN), 0);
+
+ /* A different payload must not verify against the RFC signature. */
+ c2 = jwt_checker_new();
+ ck_assert_int_eq(jwt_checker_setkey(c2, JWT_ALG_HS256, k), 0);
+ ck_assert_int_ne(jwt_checker_verify_detached(c2, TOKEN,
+ (const unsigned char *)"$.03", 4), 0);
+
+ jwks_free(ks);
+}
+END_TEST
+
+/* ---- Security + builder guards ---- */
+START_TEST(test_b64_security)
+{
+ jwk_set_t *ks;
+ const jwk_item_t *ec;
+ jwt_builder_auto_t *b = NULL;
+ jwt_checker_auto_t *c = NULL;
+ /* base64url({"alg":"ES256","b64":false}) -- b64 NOT in crit. */
+ static const char *NOCRIT =
+ "eyJhbGciOiJFUzI1NiIsImI2NCI6ZmFsc2V9.$.02.AAAA";
+
+ SET_OPS();
+ ks = load_one("ec_key_prime256v1.json");
+ ec = jwks_item_get(ks, 0);
+
+ /* @rfc{7797,6} b64=false without "b64" in "crit" is rejected. */
+ c = jwt_checker_new();
+ ck_assert_int_eq(jwt_checker_setkey(c, JWT_ALG_ES256, ec), 0);
+ ck_assert_int_ne(jwt_checker_verify(c, NOCRIT), 0);
+
+ /* The builder refuses b64=false without a raw payload (would be a JWT). */
+ b = jwt_builder_new();
+ ck_assert_int_eq(jwt_builder_setkey(b, JWT_ALG_ES256, ec), 0);
+ ck_assert_int_eq(jwt_builder_setb64(b, 0), 0);
+ ck_assert_ptr_null(jwt_builder_generate(b));
+
+ /* An unencoded payload with an embedded NUL cannot be serialized verbatim,
+ * so it is rejected rather than silently truncated. */
+ {
+ jwt_builder_auto_t *bn = jwt_builder_new();
+ static const unsigned char NUL_PT[] = { 'a', '\0', 'b' };
+
+ ck_assert_int_eq(jwt_builder_setkey(bn, JWT_ALG_ES256, ec), 0);
+ ck_assert_int_eq(jwt_builder_setpayload(bn, NUL_PT, 3), 0);
+ ck_assert_int_eq(jwt_builder_setb64(bn, 0), 0);
+ ck_assert_ptr_null(jwt_builder_generate(bn));
+ }
+
+ jwks_free(ks);
+}
+END_TEST
+
+static Suite *libjwt_suite(const char *title)
+{
+ Suite *s;
+ TCase *tc_core;
+ int i = ARRAY_SIZE(jwt_test_ops);
+
+ s = suite_create(title);
+ tc_core = tcase_create("jws_b64");
+
+ tcase_add_loop_test(tc_core, test_b64_false_attached, 0, i);
+ tcase_add_loop_test(tc_core, test_b64_false_detached, 0, i);
+ tcase_add_loop_test(tc_core, test_b64_true_detached_jwt, 0, i);
+ tcase_add_loop_test(tc_core, test_rfc7797_appendix_a, 0, i);
+ tcase_add_loop_test(tc_core, test_b64_security, 0, i);
+
+ tcase_set_timeout(tc_core, 60);
+ suite_add_tcase(s, tc_core);
+
+ return s;
+}
+
+int main(void)
+{
+ JWT_TEST_MAIN("LibJWT RFC 7797 unencoded/detached payload (#315)");
+}