]> git.ipfire.org Git - thirdparty/cups.git/commitdiff
Add cupsJWTLoadCredentials API.
authorMichael R Sweet <msweet@msweet.org>
Mon, 25 Nov 2024 21:19:46 +0000 (16:19 -0500)
committerMichael R Sweet <msweet@msweet.org>
Mon, 25 Nov 2024 21:19:46 +0000 (16:19 -0500)
Update JWT/JWS support code to handle X.509 certificates in addition to the
raw public/private key stuff - needed for some OAuth applications.

cups/jwt.c
cups/jwt.h
cups/testjwt.c

index 0369acbc937b563ca5d0536f13958e4b09fb44de..cb9ba73119081ff302cde5238f0ff3ee2fb1d598 100644 (file)
@@ -81,6 +81,7 @@ static const char * const cups_jwa_algorithms[CUPS_JWA_MAX] =
 // Local functions...
 //
 
+static cups_json_t *copy_x5c(cups_json_t *x5c);
 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);
@@ -93,7 +94,7 @@ static void   make_datstring(gnutls_datum_t *d, char *buffer, size_t bufsize);
 static gnutls_privkey_t make_private_key(cups_json_t *jwk);
 static gnutls_pubkey_t make_public_key(cups_json_t *jwk);
 #endif // HAVE_OPENSSL
-static bool    make_signature(cups_jwt_t *jwt, cups_jwa_t alg, cups_json_t *jwk, unsigned char *signature, size_t *sigsize, const char **sigkid);
+static bool    make_signature(cups_jwt_t *jwt, cups_jwa_t alg, cups_json_t *jwk, unsigned char *signature, size_t *sigsize, const char **sigkid, cups_json_t **sigx5c);
 static char    *make_string(cups_jwt_t *jwt, bool with_signature);
 
 
@@ -430,7 +431,7 @@ cupsJWTHasValidSignature(
     case CUPS_JWA_HS512 :
        // Calculate signature with keys...
        sigkid = jwt->sigkid;
-       if (!make_signature(jwt, jwt->sigalg, jwk, signature, &sigsize, &sigkid))
+       if (!make_signature(jwt, jwt->sigalg, jwk, signature, &sigsize, &sigkid, NULL))
          break;
 
        DEBUG_printf("1cupsJWTHasValidSignature: calc sig(%u) = %02X%02X%02X%02X...%02X%02X%02X%02X", (unsigned)sigsize, signature[0], signature[1], signature[2], signature[3], signature[sigsize - 4], signature[sigsize - 3], signature[sigsize - 2], signature[sigsize - 1]);
@@ -538,6 +539,8 @@ cupsJWTHasValidSignature(
        break;
   }
 
+  DEBUG_printf("1cupsJWTHasValidSignature: Returning %s.", ret ? "true" : "false");
+
   return (ret);
 }
 
@@ -745,6 +748,257 @@ cupsJWTImportString(
 }
 
 
+//
+// 'cupsJWTLoadCredentials()' - Load X.509 credentials and private key into a JSON Web Key for signing.
+//
+
+cups_json_t *                          // O - JSON Web Key of `NULL` on error
+cupsJWTLoadCredentials(
+    const char *path,                  // I - Directory path for certificate/key store or `NULL` for default
+    const char *common_name)           // I - Common name
+{
+  cups_json_t  *jwk = NULL,            // JSON Web Key
+               *x5c = NULL;            // JSON array of certificates
+  char         *key,                   // PEM-encoded private key
+               *creds,                 // PEM-encoded certificate chain
+               *current,               // Current line
+               *next,                  // Next line
+               *b64 = NULL,            // Base64 certificate value
+               *b64ptr;                // Pointer into Base64 value
+  size_t       len;                    // Length of credentials string
+  bool         isrsa;                  // Is it an RSA key?
+  char         n[1024],                // Public key modulus
+               e[1024],                // Public key exponent
+               d[1024],                // Private key exponent
+               p[1024],                // Private key first prime factor
+               q[1024],                // Private key second prime factor
+               dp[1024],               // First factor exponent
+               dq[1024],               // Second factor exponent
+               qi[1024],               // First CRT coefficient
+               x[1024],                // X coordinate
+               y[1024];                // Y coordinate
+  const char   *crv;                   // Curve value
+
+
+
+  // Load certificates and private key
+  creds = cupsCopyCredentials(path, common_name);
+  key   = cupsCopyCredentialsKey(path, common_name);
+
+  if (!creds || !key)
+    goto done;
+
+  // Convert certificate chain to an array of Base64-encoded certificates
+  len = strlen(creds);
+
+  if ((b64 = malloc(len + 1)) == NULL)
+    goto done;
+
+  x5c = cupsJSONNew(/*parent*/NULL, /*after*/NULL, CUPS_JTYPE_ARRAY);
+
+  for (current = strstr(creds, "-----BEGIN CERTIFICATE-----\n"); current; current = strstr(current, "-----BEGIN CERTIFICATE-----\n"))
+  {
+    // Extract this certificate...
+    current += 28;
+    b64ptr  = b64;
+
+    while (*current && strncmp(current, "-----", 5))
+    {
+      if ((next = strchr(current, '\n')) == NULL)
+        break;
+
+      len  = (size_t)(next - current);
+      memcpy(b64ptr, current, len);
+      b64ptr += len;
+      current = next + 1;
+    }
+
+    // Add the certificate to the array...
+    *b64ptr = '\0';
+    cupsJSONNewString(x5c, /*after*/NULL, b64);
+  }
+
+#ifdef HAVE_OPENSSL
+  EVP_PKEY     *pkey = NULL;           // Private key
+  BIO          *bio = BIO_new_mem_buf(key, -1);
+                                       // String buffer
+  const RSA    *rsa;                   // RSA key
+  const EC_KEY *ec;                            // EC key
+
+  if (!bio)
+    goto done;
+
+  PEM_read_bio_PrivateKey(bio, &pkey, /*cb*/NULL, /*u*/NULL);
+  BIO_free(bio);
+
+  if (!pkey)
+    goto done;
+
+  rsa   = EVP_PKEY_get0_RSA(pkey);
+  ec    = EVP_PKEY_get0_EC_KEY(pkey);
+  isrsa = rsa != NULL;
+
+  if (isrsa)
+  {
+    // Copy RSA key
+    make_bnstring(RSA_get0_n(rsa), n, sizeof(n));
+    make_bnstring(RSA_get0_e(rsa), e, sizeof(e));
+    make_bnstring(RSA_get0_d(rsa), d, sizeof(d));
+    make_bnstring(RSA_get0_p(rsa), p, sizeof(p));
+    make_bnstring(RSA_get0_q(rsa), q, sizeof(q));
+    make_bnstring(RSA_get0_dmp1(rsa), dp, sizeof(dp));
+    make_bnstring(RSA_get0_dmq1(rsa), dq, sizeof(dq));
+    make_bnstring(RSA_get0_iqmp(rsa), qi, sizeof(qi));
+  }
+  else if (ec)
+  {
+    // Copy EC key
+    const EC_GROUP     *group;         // Group
+    const EC_POINT     *pubkey;        // Public key portion
+    BIGNUM             *bx, *by;       // Public key coordinates
+
+    make_bnstring(EC_KEY_get0_private_key(ec), d, sizeof(d));
+
+    group  = EC_KEY_get0_group(ec);
+    pubkey = EC_KEY_get0_public_key(ec);
+
+    bx = BN_new();
+    by = BN_new();
+    EC_POINT_get_affine_coordinates(group, pubkey, bx, by, NULL);
+    make_bnstring(bx, x, sizeof(x));
+    make_bnstring(by, y, sizeof(y));
+    BN_free(bx);
+    BN_free(by);
+
+    DEBUG_printf("1cupsJWTLoadCredentials: EC_GROUP_get_curve_name(group)=%d", EC_GROUP_get_curve_name(group));
+
+    switch (EC_GROUP_get_curve_name(group))
+    {
+      case NID_secp256k1 :
+          crv = "P-256";
+          break;
+      case NID_secp384r1 :
+          crv = "P-384";
+          break;
+      case NID_secp521r1 :
+          crv = "P-521";
+          break;
+      default :
+          crv = "UNK";
+          break;
+    }
+  }
+  else
+  {
+    EVP_PKEY_free(pkey);
+    goto done;
+  }
+
+  EVP_PKEY_free(pkey);
+
+#else // HAVE_GNUTLS
+  gnutls_datum_t       dat_key;        // Private key data
+  gnutls_privkey_t     pkey;           // Private key
+  unsigned             bits;           // Private key size in bits
+
+
+  if (gnutls_privkey_init(&pkey))
+    goto done;
+
+  dat_key.data = (unsigned char *)key;
+  dat_key.size = strlen(key);
+
+  if (gnutls_privkey_import_x509_raw(pkey, &dat_key, GNUTLS_X509_FMT_PEM, /*password*/NULL, /*flags*/0))
+    goto done;
+
+  isrsa = gnutls_privkey_get_pk_algorithm(pkey, &bits) == GNUTLS_PK_RSA;
+
+  if (isrsa)
+  {
+    // Copy RSA key
+    gnutls_datum_t     dat_n, dat_e, dat_d, dat_p, dat_q, dat_dp, dat_dq, dat_qi;
+                                       // RSA parameters
+
+    gnutls_privkey_export_rsa_raw(pkey, &dat_n, &dat_e, &dat_d, &dat_p, &dat_q, &dat_qi, &dat_dp, &dat_dq);
+    make_datstring(&dat_n, n, sizeof(n));
+    make_datstring(&dat_e, e, sizeof(e));
+    make_datstring(&dat_d, d, sizeof(d));
+    make_datstring(&dat_p, p, sizeof(p));
+    make_datstring(&dat_q, q, sizeof(q));
+    make_datstring(&dat_qi, qi, sizeof(qi));
+    make_datstring(&dat_dp, dp, sizeof(dp));
+    make_datstring(&dat_dq, dq, sizeof(dq));
+  }
+  else
+  {
+    // Copy ECC key
+    gnutls_ecc_curve_t curve;          // Curve
+    gnutls_datum_t     dat_x, dat_y, dat_d;
+                                       // ECDSA parameters
+
+    gnutls_privkey_export_ecc_raw(pkey, &curve, &dat_x, &dat_y, &dat_d);
+    make_datstring(&dat_x, x, sizeof(x));
+    make_datstring(&dat_y, y, sizeof(y));
+    make_datstring(&dat_d, d, sizeof(d));
+
+    switch (curve)
+    {
+      case GNUTLS_ECC_CURVE_SECP256R1 :
+          crv = "P-256";
+          break;
+      case GNUTLS_ECC_CURVE_SECP384R1 :
+          crv = "P-384";
+          break;
+      case GNUTLS_ECC_CURVE_SECP521R1 :
+          crv = "P-521";
+          break;
+      default :
+          crv = "UNK";
+          break;
+    }
+  }
+
+  gnutls_privkey_deinit(pkey);
+#endif // HAVE_OPENSSL
+
+  // Create JWK
+  jwk = cupsJSONNew(/*parent*/NULL, /*after*/NULL, CUPS_JTYPE_OBJECT);
+
+  cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "kty"), isrsa ? "RSA" : "EC");
+  cupsJSONAdd(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "x5c"), x5c);
+
+  if (isrsa)
+  {
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "n"), n);
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "e"), e);
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "d"), d);
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "p"), p);
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "q"), q);
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "dp"), dp);
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "dq"), dq);
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "qi"), qi);
+  }
+  else
+  {
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "crv"), crv);
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "x"), x);
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "y"), y);
+    cupsJSONNewString(jwk, cupsJSONNewKey(jwk, /*after*/NULL, "d"), d);
+  }
+
+  done:
+
+  free(creds);
+  free(key);
+  free(b64);
+
+  if (!jwk)
+    cupsJSONDelete(x5c);
+
+  return (jwk);
+}
+
+
 //
 // 'cupsJWTMakePrivateKey()' - Make a JSON Web Key for encryption and signing.
 //
@@ -875,7 +1129,7 @@ cupsJWTMakePrivateKey(cups_jwa_t alg)      // I - Signing/encryption algorithm
     BIGNUM     *bx, *by;               // Public key coordinates
 
     if (alg == CUPS_JWA_ES256)
-      ec = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
+      ec = EC_KEY_new_by_curve_name(NID_secp256k1);
     else if (alg == CUPS_JWA_ES384)
       ec = EC_KEY_new_by_curve_name(NID_secp384r1);
     else
@@ -1210,6 +1464,7 @@ cupsJWTSign(cups_jwt_t  *jwt,             // I - JWT object
   size_t       sigsize = _CUPS_JWT_MAX_SIGNATURE;
                                        // Size of signature
   const char   *sigkid = NULL;         // Key ID, if any
+  cups_json_t  *sigx5c = NULL;         // X.509 certificate chain, if any
 
 
   // Range check input...
@@ -1220,10 +1475,14 @@ cupsJWTSign(cups_jwt_t  *jwt,           // I - JWT object
   }
 
   // Remove existing JOSE string, if any...
-  free(jwt->jose_string);
+  DEBUG_printf("1cupsJWTSign: jose=%p, jose_string=\"%s\"", jwt->jose, jwt->jose_string);
+
   _cupsJSONDelete(jwt->jose, "alg");
-  cupsJSONNewString(jwt->jose, cupsJSONNewKey(jwt->jose, NULL, "alg"), cups_jwa_strings[alg]);
+  _cupsJSONDelete(jwt->jose, "x5c");
+
+  cupsJSONNewString(jwt->jose, cupsJSONNewKey(jwt->jose, /*after*/NULL, "alg"), cups_jwa_strings[alg]);
 
+  free(jwt->jose_string);
   jwt->jose_string = cupsJSONExportString(jwt->jose);
 
   // Clear existing signature...
@@ -1235,12 +1494,25 @@ cupsJWTSign(cups_jwt_t  *jwt,           // I - JWT object
   jwt->sigalg    = CUPS_JWA_NONE;
 
   // Create new signature...
-  if (!make_signature(jwt, alg, jwk, signature, &sigsize, &sigkid))
+  if (!make_signature(jwt, alg, jwk, signature, &sigsize, &sigkid, &sigx5c))
   {
     DEBUG_puts("2cupsJWTSign: Unable to create signature.");
     return (false);
   }
 
+  // Update the JOSE header...
+  if (sigx5c)
+  {
+    cupsJSONAdd(jwt->jose, cupsJSONNewKey(jwt->jose, /*after*/NULL, "x5c"), copy_x5c(sigx5c));
+
+    free(jwt->jose_string);
+    jwt->jose_string = cupsJSONExportString(jwt->jose);
+    make_signature(jwt, alg, jwk, signature, &sigsize, &sigkid, NULL);
+  }
+
+  DEBUG_printf("1cupsJWTSign: jose_string=\"%s\"", jwt->jose_string);
+
+  // Save the key ID and signature values...
   if (sigkid)
     jwt->sigkid = strdup(sigkid);
 
@@ -1258,6 +1530,28 @@ cupsJWTSign(cups_jwt_t  *jwt,            // I - JWT object
 }
 
 
+//
+// 'copy_x5c()' - Copy the "x5c" (X.509 certificate chain) array.
+//
+
+static cups_json_t *                   // O - JSON array
+copy_x5c(cups_json_t *x5c)             // I - X.509 certificate chain
+{
+  size_t       i,                      // Looping var
+               count;                  // Count
+  cups_json_t  *new_x5c;               // New JSON array
+
+
+  if ((new_x5c = cupsJSONNew(/*parent*/NULL, /*after*/NULL, CUPS_JTYPE_ARRAY)) != NULL)
+  {
+    for (i = 0, count = cupsJSONGetCount(x5c); i < count; i ++)
+      cupsJSONNewString(new_x5c, /*after*/NULL, cupsJSONGetString(cupsJSONGetChild(x5c, i)));
+  }
+
+  return (new_x5c);
+}
+
+
 //
 // 'find_key()' - Find the key by name or algorithm.
 //
@@ -1394,11 +1688,13 @@ make_ec_key(cups_json_t *jwk,           // I - JSON web key
   y   = make_bignum(jwk, "y");
   d   = verify ? NULL : make_bignum(jwk, "d");
 
+  DEBUG_printf("4make_ec_key: crv=\"%s\", x=%p, y=%p, d=%p", crv, x, y, d);
+
   if (!crv || ((!x || !y) && !d))
     goto ec_done;
 
   if (!strcmp(crv, "P-256"))
-    ec = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
+    ec = EC_KEY_new_by_curve_name(NID_secp256k1);
   else if (!strcmp(crv, "P-384"))
     ec = EC_KEY_new_by_curve_name(NID_secp384r1);
   else if (!strcmp(crv, "P-521"))
@@ -1478,6 +1774,8 @@ make_rsa(cups_json_t *jwk)                // I - JSON web key
 
   rsa_done:
 
+  DEBUG_printf("4make_rsa: n=%p, e=%p, d=%p, p=%p, q=%p, dp=%p, dq=%p, qi=%p, rsa=%p", n, e, d, p, q, dp, dq, qi, rsa);
+
   if (!rsa)
   {
     BN_free(n);
@@ -1746,7 +2044,8 @@ make_signature(cups_jwt_t    *jwt,        // I  - JWT
                cups_json_t   *jwk,     // I  - JSON Web Key Set
                unsigned char *signature,// I  - Signature buffer
                size_t        *sigsize, // IO - Signature size
-               const char    **sigkid) // IO - Key ID string, if any
+               const char    **sigkid, // IO - Key ID string, if any
+               cups_json_t   **sigx5c) // O  - X.509 certificate chain, if any
 {
   bool                 ret = false;    // Return value
   char                 *text;          // JWS Signing Input
@@ -1763,7 +2062,7 @@ 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);
+  DEBUG_printf("3make_signature(jwt=%p, alg=%d, jwk=%p, signature=%p, sigsize=%p(%u), sigkid=%p(%s), sigx5c=%p)", (void *)jwt, alg, (void *)jwk, (void *)signature, (void *)sigsize, (unsigned)*sigsize, (void *)sigkid, *sigkid, (void *)sigx5c);
 
   // Get text to sign...
   text     = make_string(jwt, false);
@@ -1923,9 +2222,15 @@ make_signature(cups_jwt_t    *jwt,       // I  - JWT
   free(text);
 
   if (ret)
+  {
     *sigkid = cupsJSONGetString(cupsJSONFind(jwk, "kid"));
+    if (sigx5c)
+      *sigx5c = cupsJSONFind(jwk, "x5c");
+  }
   else
+  {
     *sigsize = 0;
+  }
 
   return (ret);
 }
@@ -1951,6 +2256,8 @@ make_string(cups_jwt_t *jwt,              // I - JWT object
   if (!jwt->claims_string)
     jwt->claims_string = cupsJSONExportString(jwt->claims);
 
+  DEBUG_printf("4make_string: jose_string=\"%s\", claims_string=\"%s\"", jwt->jose_string, jwt->claims_string);
+
   if (!jwt->jose_string || !jwt->claims_string)
     return (NULL);
 
index e82b28dfb52b08b945d118b916c9bcd8b291f16e..1fdbab33e4d7cb96a06c3cf3627a5b20e75a2362 100644 (file)
@@ -76,6 +76,7 @@ extern cups_json_t    *cupsJWTGetHeaderValue(cups_jwt_t *jwt, const char *claim) _C
 extern cups_json_t     *cupsJWTGetHeaders(cups_jwt_t *jwt) _CUPS_PUBLIC;
 extern bool            cupsJWTHasValidSignature(cups_jwt_t *jwt, cups_json_t *keys) _CUPS_PUBLIC;
 extern cups_jwt_t      *cupsJWTImportString(const char *s, cups_jws_format_t format) _CUPS_PUBLIC;
+extern cups_json_t     *cupsJWTLoadCredentials(const char *path, const char *common_name) _CUPS_PUBLIC;
 extern cups_json_t     *cupsJWTMakePrivateKey(cups_jwa_t alg) _CUPS_PUBLIC;
 extern cups_json_t     *cupsJWTMakePublicKey(cups_json_t *jwk) _CUPS_PUBLIC;
 extern cups_jwt_t      *cupsJWTNew(const char *type) _CUPS_PUBLIC;
index 1be97b8f88957f3eb4f3a6021faf035607d9b4fd..1d4c82d57baef284b0208443b7187bd9f528b1ff 100644 (file)
@@ -23,6 +23,7 @@ main(int  argc,                               // I - Number of command-line arguments
   int          i;                      // Looping var
   cups_jwt_t   *jwt;                   // JSON Web Token object
   cups_json_t  *jwk;                   // JSON Web Key Set
+  char         *s;                     // Temporary string
 
 
   if (argc == 1)
@@ -82,7 +83,8 @@ main(int  argc,                               // I - Number of command-line arguments
             "W0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U\""
             "}"
       },
-      {
+      // P-256 example uses wrong curve...
+      /*{
         "eyJhbGciOiJFUzI1NiJ9."
             "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt"
             "cGxlLmNvbS9pc19yb290Ijp0cnVlfQ."
@@ -94,7 +96,7 @@ main(int  argc,                               // I - Number of command-line arguments
             "\"y\":\"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0\","
             "\"d\":\"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI\""
             "}"
-      }
+      }*/
     };
 
     testBegin("cupsJWTNew(NULL)");
@@ -287,6 +289,94 @@ main(int  argc,                            // I - Number of command-line arguments
       cupsJWTDelete(jwt);
     }
 
+    testBegin("cupsCreateCredentials(xxx-rsa-2048)");
+    if (cupsCreateCredentials(/*path*/NULL, /*ca_cert*/false, CUPS_CREDPURPOSE_CLIENT_AUTH | CUPS_CREDPURPOSE_SERVER_AUTH, CUPS_CREDTYPE_RSA_2048_SHA256, CUPS_CREDUSAGE_DEFAULT_TLS, /*organization*/NULL, /*org_unit*/NULL, /*locality*/NULL, /*state_province*/NULL, /*country*/NULL, "xxx-rsa-2048", /*email*/NULL, /*num_alt_names*/0, /*alt_names*/NULL, /*root_name*/NULL, /*expiration_date*/time(NULL) + 86400))
+    {
+      testEnd(true);
+
+      testBegin("cupsJWTLoadCredentials(xxx-rsa-2048)");
+      if ((jwk = cupsJWTLoadCredentials(/*path*/NULL, "xxx-rsa-2048")) != NULL)
+      {
+       testEnd(true);
+       s = cupsJSONExportString(jwk);
+       testMessage("jwk=\"%s\"", s);
+       free(s);
+
+       testBegin("cupsJWTNew(NULL)");
+       jwt = cupsJWTNew(NULL);
+       testEnd(jwt != NULL);
+
+       testBegin("cupsJWTSetClaimNumber(CUPS_JWT_IAT)");
+       cupsJWTSetClaimNumber(jwt, CUPS_JWT_IAT, (double)time(NULL) + 86400.0);
+       testEnd(cupsJWTGetClaimNumber(jwt, CUPS_JWT_IAT) > 0.0);
+
+       testBegin("cupsJWTSetClaimString(CUPS_JWT_SUB)");
+       cupsJWTSetClaimString(jwt, CUPS_JWT_SUB, "joe.user");
+       testEnd(cupsJWTGetClaimString(jwt, CUPS_JWT_SUB) != NULL);
+
+       testBegin("cupsJWTSign(RS256)");
+       testEnd(cupsJWTSign(jwt, CUPS_JWA_RS256, jwk));
+
+       testBegin("cupsJWTHasValidSignature(RS256)");
+       testEnd(cupsJWTHasValidSignature(jwt, jwk));
+
+        cupsJWTDelete(jwt);
+        cupsJSONDelete(jwk);
+      }
+      else
+      {
+        testEndMessage(false, "%s", cupsGetErrorString());
+      }
+    }
+    else
+    {
+      testEndMessage(false, "%s", cupsGetErrorString());
+    }
+
+    testBegin("cupsCreateCredentials(xxx-ecdsa-p256)");
+    if (cupsCreateCredentials(/*path*/NULL, /*ca_cert*/false, CUPS_CREDPURPOSE_CLIENT_AUTH | CUPS_CREDPURPOSE_SERVER_AUTH, CUPS_CREDTYPE_ECDSA_P256_SHA256, CUPS_CREDUSAGE_DEFAULT_TLS, /*organization*/NULL, /*org_unit*/NULL, /*locality*/NULL, /*state_province*/NULL, /*country*/NULL, "xxx-ecdsa-p256", /*email*/NULL, /*num_alt_names*/0, /*alt_names*/NULL, /*root_name*/NULL, /*expiration_date*/time(NULL) + 86400))
+    {
+      testEnd(true);
+
+      testBegin("cupsJWTLoadCredentials(xxx-ecdsa-p256)");
+      if ((jwk = cupsJWTLoadCredentials(/*path*/NULL, "xxx-ecdsa-p256")) != NULL)
+      {
+       testEnd(true);
+       s = cupsJSONExportString(jwk);
+       testMessage("jwk=\"%s\"", s);
+       free(s);
+
+       testBegin("cupsJWTNew(NULL)");
+       jwt = cupsJWTNew(NULL);
+       testEnd(jwt != NULL);
+
+       testBegin("cupsJWTSetClaimNumber(CUPS_JWT_IAT)");
+       cupsJWTSetClaimNumber(jwt, CUPS_JWT_IAT, (double)time(NULL) + 86400.0);
+       testEnd(cupsJWTGetClaimNumber(jwt, CUPS_JWT_IAT) > 0.0);
+
+       testBegin("cupsJWTSetClaimString(CUPS_JWT_SUB)");
+       cupsJWTSetClaimString(jwt, CUPS_JWT_SUB, "joe.user");
+       testEnd(cupsJWTGetClaimString(jwt, CUPS_JWT_SUB) != NULL);
+
+       testBegin("cupsJWTSign(ES256)");
+       testEnd(cupsJWTSign(jwt, CUPS_JWA_ES256, jwk));
+
+       testBegin("cupsJWTHasValidSignature(ES256)");
+       testEnd(cupsJWTHasValidSignature(jwt, jwk));
+
+        cupsJWTDelete(jwt);
+        cupsJSONDelete(jwk);
+      }
+      else
+      {
+        testEndMessage(false, "%s", cupsGetErrorString());
+      }
+    }
+    else
+    {
+      testEndMessage(false, "%s", cupsGetErrorString());
+    }
+
     if (!testsPassed)
       return (1);
   }