Skip to content

Commit

Permalink
Fix JWT signature validation/tracking - needed to support "kid" for R…
Browse files Browse the repository at this point in the history
…SA and

ECDSA algorithms, needed to set the "kid" value when importing.

Show claims and JOSE header values, support JWKS and validation in testjwt
code.
  • Loading branch information
michaelrsweet committed Sep 9, 2024
1 parent 2c506ed commit 7404a8e
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 55 deletions.
145 changes: 94 additions & 51 deletions cups/jwt.c
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ static const char * const cups_jwa_algorithms[CUPS_JWA_MAX] =
// Local functions...
//

static cups_json_t *find_key(cups_json_t *jwk, cups_jwa_t sigalg, const char *kid);
#ifdef HAVE_OPENSSL
static BIGNUM *make_bignum(cups_json_t *jwk, const char *key);
static void make_bnstring(const BIGNUM *bn, char *buffer, size_t bufsize);
Expand Down Expand Up @@ -392,7 +393,7 @@ cupsJWTHasValidSignature(
if (!jwt || !jwt->signature || !jwk)
return (false);

DEBUG_printf("1cupsJWTHasValidSignature: orig sig(%u) = %02X%02X%02X%02X...%02X%02X%02X%02X", (unsigned)jwt->sigsize, jwt->signature[0], jwt->signature[1], jwt->signature[2], jwt->signature[3], jwt->signature[jwt->sigsize - 4], jwt->signature[jwt->sigsize - 3], jwt->signature[jwt->sigsize - 2], jwt->signature[jwt->sigsize - 1]);
DEBUG_printf("1cupsJWTHasValidSignature: sigalg=%d, orig sig[%u]=<%02X%02X%02X%02X...%02X%02X%02X%02X>", jwt->sigalg, (unsigned)jwt->sigsize, jwt->signature[0], jwt->signature[1], jwt->signature[2], jwt->signature[3], jwt->signature[jwt->sigsize - 4], jwt->signature[jwt->sigsize - 3], jwt->signature[jwt->sigsize - 2], jwt->signature[jwt->sigsize - 1]);

switch (jwt->sigalg)
{
Expand All @@ -416,6 +417,7 @@ cupsJWTHasValidSignature(
// Get the message hash...
text = make_string(jwt, false);
text_len = strlen(text);
jwk = find_key(jwk, jwt->sigalg, jwt->sigkid);

#ifdef HAVE_OPENSSL
hash_len = cupsHashData(cups_jwa_algorithms[jwt->sigalg], text, text_len, hash, sizeof(hash));
Expand Down Expand Up @@ -451,6 +453,7 @@ cupsJWTHasValidSignature(
// Get the message hash...
text = make_string(jwt, false);
text_len = strlen(text);
jwk = find_key(jwk, jwt->sigalg, jwt->sigkid);

#ifdef HAVE_OPENSSL
hash_len = cupsHashData(cups_jwa_algorithms[jwt->sigalg], text, text_len, hash, sizeof(hash));
Expand Down Expand Up @@ -523,7 +526,8 @@ cupsJWTImportString(
cups_jwt_t *jwt; // JWT object
size_t datalen; // Size of data
char data[65536]; // Data
const char *alg; // Signature algorithm, if any
const char *kid, // Key identifier
*alg; // Signature algorithm, if any


// Allocate a JWT...
Expand Down Expand Up @@ -577,8 +581,6 @@ cupsJWTImportString(
// Import JSON...
cups_json_t *json, // JSON data
*json_value, // BASE64URL-encoded string value node
*header, // Unprotected header
*kid, // Key ID node
*signatures, // Signatures array
*signature; // Signature element to load
const char *value, // C string value
Expand Down Expand Up @@ -664,26 +666,39 @@ cupsJWTImportString(
memcpy(jwt->signature, data, datalen);
jwt->sigsize = datalen;
}

if ((header = cupsJSONFind(signature, "header")) != NULL && (kid = cupsJSONFind(header, "kid")) != NULL && (value = cupsJSONGetString(kid)) != NULL)
jwt->sigkid = strdup(value);
}

#ifdef DEBUG
if (jwt->sigsize >= 8)
DEBUG_printf("1cupsJWTImportString: signature[%u]=<%02X%02X%02X%02X...%02X%02X%02X%02X>", (unsigned)jwt->sigsize, jwt->signature[0], jwt->signature[1], jwt->signature[2], jwt->signature[3], jwt->signature[jwt->sigsize - 4], jwt->signature[jwt->sigsize - 3], jwt->signature[jwt->sigsize - 2], jwt->signature[jwt->sigsize - 1]);
else if (jwt->sigsize > 0)
DEBUG_printf("1cupsJWTImportString: signature[%u]=<...>", (unsigned)jwt->sigsize);
#endif // DEBUG

// Check the algorithm used in the protected header...
if ((alg = cupsJSONGetString(cupsJSONFind(jwt->jose, "alg"))) != NULL)
{
cups_jwa_t sigalg; // Signing algorithm

DEBUG_printf("1cupsJWTImportString: alg=\"%s\"", alg);

for (sigalg = CUPS_JWA_NONE; sigalg < CUPS_JWA_MAX; sigalg ++)
{
if (!strcmp(alg, cups_jwa_strings[sigalg]))
{
jwt->sigalg = sigalg;
DEBUG_printf("1cupsJWTImportString: sigalg=%d", sigalg);
break;
}
}
}

if ((kid = cupsJSONGetString(cupsJSONFind(jwt->jose, "kid"))) != NULL)
{
DEBUG_printf("1cupsJWTImportString: kid=\"%s\"", kid);
jwt->sigkid = strdup(kid);
}

// Can't have signature with none or no signature for !none...
if ((jwt->sigalg == CUPS_JWA_NONE) != (jwt->sigsize == 0))
goto import_error;
Expand Down Expand Up @@ -1193,6 +1208,67 @@ cupsJWTSign(cups_jwt_t *jwt, // I - JWT object
}


//
// 'find_key()' - Find the key by name or algorithm.
//

static cups_json_t * // O - Key data
find_key(cups_json_t *jwk, // I - Key set
cups_jwa_t alg, // I - Signature algorithm
const char *kid) // I - Signature key ID
{
cups_json_t *keys; // Array of keys


if ((keys = cupsJSONFind(jwk, "keys")) != NULL)
{
// Full key set, find the key we need to use...
size_t i, // Looping var
count; // Number of keys
cups_json_t *current; // Current key
const char *curkid, // Current key ID
*curkty; // Current key type

count = cupsJSONGetCount(keys);

if (kid)
{
// Find the matching key ID
for (i = 0; i < count; i ++)
{
current = cupsJSONGetChild(keys, i);
curkid = cupsJSONGetString(cupsJSONFind(current, "kid"));

if (curkid && !strcmp(curkid, kid))
{
DEBUG_printf("4make_signature: Found matching key \"%s\" at %p.", curkid, (void *)current);
jwk = current;
break;
}
}
}
else
{
// Find a key that can be used for the specified algorithm
for (i = 0; i < count; i ++)
{
current = cupsJSONGetChild(keys, i);
curkty = cupsJSONGetString(cupsJSONFind(current, "kty"));

if (((!curkty || !strcmp(curkty, "ocy")) && alg >= CUPS_JWA_HS256 && alg <= CUPS_JWA_HS512) || (curkty && !strcmp(curkty, "RSA") && alg >= CUPS_JWA_RS256 && alg <= CUPS_JWA_RS512) || (curkty && !strcmp(curkty, "EC") && alg >= CUPS_JWA_ES256 && alg <= CUPS_JWA_ES512))
{
DEBUG_printf("4make_signature: Found compatible key \"%s\" at %p.", cupsJSONGetString(cupsJSONFind(current, "kid")), (void *)current);
jwk = current;
break;
}
}
}
}

return (jwk);
}


#ifdef HAVE_OPENSSL
//
// 'make_bignum()' - Make a BIGNUM for the specified key.
Expand Down Expand Up @@ -1623,7 +1699,6 @@ make_signature(cups_jwt_t *jwt, // I - JWT
const char **sigkid) // IO - Key ID string, if any
{
bool ret = false; // Return value
cups_json_t *keys; // Array of keys
char *text; // JWS Signing Input
size_t text_len; // Length of signing input
#ifdef HAVE_OPENSSL
Expand All @@ -1638,52 +1713,12 @@ make_signature(cups_jwt_t *jwt, // I - JWT
#endif // HAVE_OPENSSL


DEBUG_printf("3make_signature(jwt=%p, alg=%d, jwk=%p, signature=%p, sigsize=%p(%u), sigkid=%p(%s))", (void *)jwt, alg, (void *)jwk, (void *)signature, (void *)sigsize, (unsigned)*sigsize, (void *)sigkid, *sigkid);

// Get text to sign...
text = make_string(jwt, false);
text_len = strlen(text);

if ((keys = cupsJSONFind(jwk, "keys")) != NULL)
{
// Full key set, find the key we need to use...
size_t i, // Looping var
count; // Number of keys
cups_json_t *current; // Current key
const char *curkid, // Current key ID
*curkty; // Current key type

count = cupsJSONGetCount(keys);

if (*sigkid)
{
// Find the matching key ID
for (i = 0; i < count; i ++)
{
current = cupsJSONGetChild(keys, i);
curkid = cupsJSONGetString(cupsJSONFind(current, "kid"));

if (curkid && !strcmp(curkid, *sigkid))
{
jwk = current;
break;
}
}
}
else
{
// Find a key that can be used for the specified algorithm
for (i = 0; i < count; i ++)
{
current = cupsJSONGetChild(keys, i);
curkty = cupsJSONGetString(cupsJSONFind(current, "kty"));

if (((!curkty || !strcmp(curkty, "ocy")) && alg >= CUPS_JWA_HS256 && alg <= CUPS_JWA_HS512) || (curkty && !strcmp(curkty, "RSA") && alg >= CUPS_JWA_RS256 && alg <= CUPS_JWA_RS512) || (curkty && !strcmp(curkty, "EC") && alg >= CUPS_JWA_ES256 && alg <= CUPS_JWA_ES512))
{
jwk = current;
break;
}
}
}
}
jwk = find_key(jwk, alg, *sigkid);

if (alg >= CUPS_JWA_HS256 && alg <= CUPS_JWA_HS512)
{
Expand All @@ -1693,6 +1728,8 @@ make_signature(cups_jwt_t *jwt, // I - JWT
size_t key_len; // Length of key
ssize_t hmac_len; // Length of HMAC

DEBUG_puts("4make_signature: HMAC signature");

// Get key...
memset(key, 0, sizeof(key));
k = cupsJSONGetString(cupsJSONFind(jwk, "k"));
Expand All @@ -1709,6 +1746,8 @@ make_signature(cups_jwt_t *jwt, // I - JWT
else if (alg >= CUPS_JWA_RS256 && alg <= CUPS_JWA_RS512)
{
// RSASSA-PKCS1-v1_5 SHA-256/384/512
DEBUG_puts("4make_signature: RSA signature");

#ifdef HAVE_OPENSSL
unsigned char hash[128]; // SHA-256/384/512 hash
ssize_t hash_len; // Length of hash
Expand Down Expand Up @@ -1750,6 +1789,8 @@ make_signature(cups_jwt_t *jwt, // I - JWT
else if (alg >= CUPS_JWA_ES256 && alg <= CUPS_JWA_ES512)
{
// ECDSA P-256 SHA-256/384/512
DEBUG_puts("4make_signature: ECDSA signature");

static unsigned sig_sizes[3] = // Sizes of signatures
{ 64, 96, 132 };
#ifdef HAVE_OPENSSL
Expand Down Expand Up @@ -1827,6 +1868,8 @@ make_signature(cups_jwt_t *jwt, // I - JWT

done:

DEBUG_printf("4make_signature: Returning %s.", ret ? "true" : "false");

free(text);

if (ret)
Expand Down
67 changes: 64 additions & 3 deletions cups/testjwt.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//
// JWT API unit tests for CUPS.
//
// Copyright © 2023 by OpenPrinting.
// Copyright © 2023-2024 by OpenPrinting.
//
// Licensed under Apache License v2.0. See the file "LICENSE" for more
// information.
Expand Down Expand Up @@ -293,11 +293,72 @@ main(int argc, // I - Number of command-line arguments
else
{
// Try loading JWT string on the command-line...
cups_json_t *jwks = NULL; // JWT Key Set, if any

for (i = 1; i < argc; i ++)
{
if ((jwt = cupsJWTImportString(argv[i], CUPS_JWS_FORMAT_COMPACT)) != NULL)
if (!access(argv[i], R_OK))
{
if ((jwks = cupsJSONImportFile(argv[i])) == NULL)
{
fprintf(stderr, "%s: %s\n", argv[i], cupsGetErrorString());
return (1);
}
}
else if ((jwt = cupsJWTImportString(argv[i], CUPS_JWS_FORMAT_COMPACT)) != NULL)
{
// printf("%s: OK, %u key/value pairs in root object.\n", argv[i], (unsigned)(cupsJSONGetCount(json) / 2));
cups_json_t *claims = cupsJWTGetClaims(jwt);
// All claims
cups_json_t *headers = cupsJWTGetHeaders(jwt);
// All JOSE headers
char *temp; // Temporary string
const char *aud = cupsJWTGetClaimString(jwt, CUPS_JWT_AUD);
// Audience
const char *iss = cupsJWTGetClaimString(jwt, CUPS_JWT_ISS);
// Issuer
const char *jti = cupsJWTGetClaimString(jwt, CUPS_JWT_JTI);
// JWT ID
const char *name = cupsJWTGetClaimString(jwt, CUPS_JWT_NAME);
// Display name
const char *sub = cupsJWTGetClaimString(jwt, CUPS_JWT_SUB);
// Subject (username/ID)
double iat = cupsJWTGetClaimNumber(jwt, CUPS_JWT_IAT);
// Issue time
double exp = cupsJWTGetClaimNumber(jwt, CUPS_JWT_EXP);
// Expiration time
double nbf = cupsJWTGetClaimNumber(jwt, CUPS_JWT_NBF);
// Not before time
char date[256]; // Date

if (iss)
printf("Issuer: %s\n", iss);
if (name)
printf("Display Name: %s\n", name);
if (sub)
printf("Subject: %s\n", sub);
if (aud)
printf("Audience: %s\n", aud);
if (jti)
printf("JWT ID: %s\n", jti);
if (iat > 0.0)
printf("Issued On: %s\n", httpGetDateString((time_t)iat, date, sizeof(date)));
if (exp > 0.0)
printf("Expires On: %s\n", httpGetDateString((time_t)exp, date, sizeof(date)));
if (nbf > 0.0)
printf("Not Before: %s\n", httpGetDateString((time_t)nbf, date, sizeof(date)));
printf("Valid: %s\n", jwks ? (cupsJWTHasValidSignature(jwt, jwks) ? "yes" : "no") : "unknown");

if ((temp = cupsJSONExportString(headers)) != NULL)
{
printf("\njose=%s\n", temp);
free(temp);
}

if ((temp = cupsJSONExportString(claims)) != NULL)
{
printf("\nclaims=%s\n", temp);
free(temp);
}

cupsJWTDelete(jwt);
}
Expand Down
2 changes: 1 addition & 1 deletion pdfio

0 comments on commit 7404a8e

Please sign in to comment.