346 lines
11 KiB
C
346 lines
11 KiB
C
/*
|
|
* Copyright (c) 2025 Kungliga Tekniska Högskolan
|
|
* (Royal Institute of Technology, Stockholm, Sweden).
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
*
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* 3. Neither the name of the Institute nor the names of its contributors
|
|
* may be used to endorse or promote products derived from this software
|
|
* without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*/
|
|
|
|
/*
|
|
* Test program for JWT validator using RFC 7515 and RFC 8037 test vectors.
|
|
*/
|
|
|
|
#include <config.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
|
|
#include <krb5.h>
|
|
#include "jwt_validator.h"
|
|
|
|
/*
|
|
* RFC 7515 Appendix A.2 - RS256 test vector
|
|
*
|
|
* The token has claims: {"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}
|
|
* Note: exp is in the past (2011), so we test signature only, not expiration.
|
|
*/
|
|
static const char *rfc7515_rs256_token =
|
|
"eyJhbGciOiJSUzI1NiJ9"
|
|
"."
|
|
"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
|
|
"."
|
|
"cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw";
|
|
|
|
/* RSA public key from RFC 7515 A.2 in PEM format */
|
|
static const char *rfc7515_rs256_pubkey_pem =
|
|
"-----BEGIN PUBLIC KEY-----\n"
|
|
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAofgWCuLjybRlzo0tZWJj\n"
|
|
"NiuSfb4p4fAkd/wWJcyQoTbji9k0l8W26mPddxHmfHQp+Vaw+4qPCJrcS2mJPMEz\n"
|
|
"P1Pt0Bm4d4QlL+yRT+SFd2lZS+pCgNMsD1W/YpRPEwOWvG6b32690r2jZ47soMZo\n"
|
|
"9wGzjb/7OMg0LOL+bSf63kpaSHSXndS5z5rexMdbBYUsLA9e+KXBdQOS+UTo7WTB\n"
|
|
"EMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6/I5IhlJH7aGhyxX\n"
|
|
"FvUK+DWNmoudF8NAco9/h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXp\n"
|
|
"oQIDAQAB\n"
|
|
"-----END PUBLIC KEY-----\n";
|
|
|
|
/*
|
|
* RFC 7515 Appendix A.3 - ES256 test vector
|
|
*
|
|
* Same claims as RS256.
|
|
*/
|
|
static const char *rfc7515_es256_token =
|
|
"eyJhbGciOiJFUzI1NiJ9"
|
|
"."
|
|
"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
|
|
"."
|
|
"DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q";
|
|
|
|
/* EC P-256 public key from RFC 7515 A.3 in PEM format */
|
|
static const char *rfc7515_es256_pubkey_pem =
|
|
"-----BEGIN PUBLIC KEY-----\n"
|
|
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf83OJ3D2xF1Bg8vub9tLe1gHMzV7\n"
|
|
"6e8Tus9uPHvRVEXH8UTNG72bfocs3+257rn0s2ldbqkLJK2KRiMohYjlrQ==\n"
|
|
"-----END PUBLIC KEY-----\n";
|
|
|
|
/*
|
|
* RFC 8037 Appendix A.4 - EdDSA (Ed25519) test vector
|
|
*
|
|
* Payload is "Example of Ed25519 signing" (not JSON claims)
|
|
*/
|
|
static const char *rfc8037_eddsa_token =
|
|
"eyJhbGciOiJFZERTQSJ9"
|
|
"."
|
|
"RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc"
|
|
"."
|
|
"hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg";
|
|
|
|
/* Ed25519 public key from RFC 8037 A.4 in PEM format */
|
|
static const char *rfc8037_ed25519_pubkey_pem =
|
|
"-----BEGIN PUBLIC KEY-----\n"
|
|
"MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=\n"
|
|
"-----END PUBLIC KEY-----\n";
|
|
|
|
static int
|
|
write_temp_key(const char *pem, char *path, size_t pathlen)
|
|
{
|
|
int fd;
|
|
ssize_t len = strlen(pem);
|
|
|
|
snprintf(path, pathlen, "/tmp/jwt_test_key_XXXXXX");
|
|
fd = mkstemp(path);
|
|
if (fd < 0)
|
|
return -1;
|
|
if (write(fd, pem, len) != len) {
|
|
close(fd);
|
|
unlink(path);
|
|
return -1;
|
|
}
|
|
close(fd);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Test signature verification only (not claims validation).
|
|
* We pass no audiences and ignore expiration since RFC test vectors
|
|
* have expired exp claims.
|
|
*/
|
|
static int
|
|
test_signature_only(krb5_context context,
|
|
const char *name,
|
|
const char *token,
|
|
const char *pubkey_pem)
|
|
{
|
|
char keypath[256];
|
|
const char *paths[1];
|
|
krb5_boolean result = FALSE;
|
|
krb5_principal princ = NULL;
|
|
krb5_times times;
|
|
krb5_error_code ret;
|
|
|
|
printf("Testing %s signature verification... ", name);
|
|
fflush(stdout);
|
|
|
|
if (write_temp_key(pubkey_pem, keypath, sizeof(keypath)) < 0) {
|
|
printf("FAILED (could not write temp key)\n");
|
|
return 1;
|
|
}
|
|
|
|
paths[0] = keypath;
|
|
|
|
/*
|
|
* Note: validate_jwt_token validates exp/nbf claims, but the RFC
|
|
* test vectors have expired tokens. We're primarily testing signature
|
|
* verification here, so we accept EACCES for expired tokens as long
|
|
* as we get past signature verification.
|
|
*/
|
|
ret = validate_jwt_token(context,
|
|
token, strlen(token),
|
|
paths, 1,
|
|
NULL, 0, /* no audience check */
|
|
&result, &princ, ×,
|
|
"TEST.REALM");
|
|
|
|
unlink(keypath);
|
|
|
|
/*
|
|
* For RFC test vectors, we expect either:
|
|
* - Success (result == TRUE)
|
|
* - EACCES with "expired" in the error message (signature was valid)
|
|
* - EACCES with "no subject" (signature was valid, but payload isn't JWT claims)
|
|
* - EINVAL with "could not parse claims" (signature was valid, but payload isn't JSON)
|
|
*/
|
|
if (ret == 0 && result) {
|
|
printf("OK\n");
|
|
krb5_free_principal(context, princ);
|
|
return 0;
|
|
}
|
|
|
|
if (ret == EACCES || ret == EINVAL) {
|
|
const char *msg = krb5_get_error_message(context, ret);
|
|
if (strstr(msg, "expired") || strstr(msg, "no subject") ||
|
|
strstr(msg, "not valid JSON") || strstr(msg, "could not parse claims")) {
|
|
printf("OK (signature valid, %s)\n",
|
|
strstr(msg, "expired") ? "token expired" : "non-JWT payload");
|
|
krb5_free_error_message(context, msg);
|
|
krb5_free_principal(context, princ);
|
|
return 0;
|
|
}
|
|
printf("FAILED: %s\n", msg);
|
|
krb5_free_error_message(context, msg);
|
|
} else if (ret) {
|
|
const char *msg = krb5_get_error_message(context, ret);
|
|
printf("FAILED: %s\n", msg);
|
|
krb5_free_error_message(context, msg);
|
|
} else {
|
|
printf("FAILED: result=%d\n", result);
|
|
}
|
|
|
|
krb5_free_principal(context, princ);
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Test with a tampered token (should fail signature verification).
|
|
*/
|
|
static int
|
|
test_tampered_token(krb5_context context)
|
|
{
|
|
char keypath[256];
|
|
const char *paths[1];
|
|
krb5_boolean result = FALSE;
|
|
krb5_principal princ = NULL;
|
|
krb5_times times;
|
|
krb5_error_code ret;
|
|
char *tampered;
|
|
|
|
printf("Testing tampered token rejection... ");
|
|
fflush(stdout);
|
|
|
|
/* Copy and tamper with the token (change one character in payload) */
|
|
tampered = strdup(rfc7515_rs256_token);
|
|
if (!tampered) {
|
|
printf("FAILED (out of memory)\n");
|
|
return 1;
|
|
}
|
|
/* Find the payload and change a character */
|
|
tampered[50] = (tampered[50] == 'a') ? 'b' : 'a';
|
|
|
|
if (write_temp_key(rfc7515_rs256_pubkey_pem, keypath, sizeof(keypath)) < 0) {
|
|
printf("FAILED (could not write temp key)\n");
|
|
free(tampered);
|
|
return 1;
|
|
}
|
|
|
|
paths[0] = keypath;
|
|
|
|
ret = validate_jwt_token(context,
|
|
tampered, strlen(tampered),
|
|
paths, 1,
|
|
NULL, 0,
|
|
&result, &princ, ×,
|
|
"TEST.REALM");
|
|
|
|
unlink(keypath);
|
|
free(tampered);
|
|
|
|
if (ret == EPERM && !result) {
|
|
printf("OK (correctly rejected)\n");
|
|
return 0;
|
|
}
|
|
|
|
printf("FAILED: tampered token was accepted!\n");
|
|
krb5_free_principal(context, princ);
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Test wrong key rejection.
|
|
*/
|
|
static int
|
|
test_wrong_key(krb5_context context)
|
|
{
|
|
char keypath[256];
|
|
const char *paths[1];
|
|
krb5_boolean result = FALSE;
|
|
krb5_principal princ = NULL;
|
|
krb5_times times;
|
|
krb5_error_code ret;
|
|
|
|
printf("Testing wrong key rejection... ");
|
|
fflush(stdout);
|
|
|
|
/* Use ES256 key to verify RS256 token - should fail */
|
|
if (write_temp_key(rfc7515_es256_pubkey_pem, keypath, sizeof(keypath)) < 0) {
|
|
printf("FAILED (could not write temp key)\n");
|
|
return 1;
|
|
}
|
|
|
|
paths[0] = keypath;
|
|
|
|
ret = validate_jwt_token(context,
|
|
rfc7515_rs256_token, strlen(rfc7515_rs256_token),
|
|
paths, 1,
|
|
NULL, 0,
|
|
&result, &princ, ×,
|
|
"TEST.REALM");
|
|
|
|
unlink(keypath);
|
|
|
|
if (ret == EPERM && !result) {
|
|
printf("OK (correctly rejected)\n");
|
|
return 0;
|
|
}
|
|
|
|
printf("FAILED: wrong key type was accepted!\n");
|
|
krb5_free_principal(context, princ);
|
|
return 1;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
krb5_context context;
|
|
krb5_error_code ret;
|
|
int failures = 0;
|
|
|
|
ret = krb5_init_context(&context);
|
|
if (ret) {
|
|
fprintf(stderr, "krb5_init_context failed: %d\n", ret);
|
|
return 1;
|
|
}
|
|
|
|
printf("JWT Validator Test Suite\n");
|
|
printf("========================\n\n");
|
|
|
|
printf("RFC 7515 Test Vectors:\n");
|
|
failures += test_signature_only(context, "RS256", rfc7515_rs256_token,
|
|
rfc7515_rs256_pubkey_pem);
|
|
failures += test_signature_only(context, "ES256", rfc7515_es256_token,
|
|
rfc7515_es256_pubkey_pem);
|
|
|
|
printf("\nRFC 8037 Test Vectors:\n");
|
|
failures += test_signature_only(context, "EdDSA (Ed25519)", rfc8037_eddsa_token,
|
|
rfc8037_ed25519_pubkey_pem);
|
|
|
|
printf("\nNegative Tests:\n");
|
|
failures += test_tampered_token(context);
|
|
failures += test_wrong_key(context);
|
|
|
|
printf("\n");
|
|
if (failures == 0) {
|
|
printf("All tests passed!\n");
|
|
} else {
|
|
printf("%d test(s) failed!\n", failures);
|
|
}
|
|
|
|
krb5_free_context(context);
|
|
return failures ? 1 : 0;
|
|
}
|