/* * Copyright (c) 2019-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. */ /* * JOSE (JSON Object Signing and Encryption) support. * * This implements: * - JWS (JSON Web Signature) - RFC 7515 * - JWT (JSON Web Token) - RFC 7519 * - JWK (JSON Web Key) - RFC 7517 * * Supported algorithms: * - RS256, RS384, RS512 (RSASSA-PKCS1-v1_5) * - ES256, ES384, ES512 (ECDSA) * - EdDSA (Ed25519, Ed448) */ #include "hx_locl.h" #include #include #include #include #include #include #include #include /* JWS signature algorithms */ typedef enum hx509_jws_alg { HX509_JWS_ALG_NONE = 0, HX509_JWS_ALG_RS256, HX509_JWS_ALG_RS384, HX509_JWS_ALG_RS512, HX509_JWS_ALG_ES256, HX509_JWS_ALG_ES384, HX509_JWS_ALG_ES512, HX509_JWS_ALG_EDDSA, HX509_JWS_ALG_UNKNOWN } hx509_jws_alg; /* * Base64URL decoding helper - allocates and returns decoded data. * rk_base64url_decode() requires pre-allocated buffer, so this wrapper * is convenient for callers. */ static unsigned char * base64url_decode(const char *str, size_t *out_len) { unsigned char *data; size_t len; int decoded_len; if (str == NULL) return NULL; len = strlen(str); /* Decoded data is at most 3/4 of encoded length */ data = malloc(len + 1); if (data == NULL) return NULL; decoded_len = rk_base64url_decode(str, data); if (decoded_len < 0) { free(data); return NULL; } *out_len = decoded_len; return data; } /* * Algorithm name parsing */ static hx509_jws_alg parse_alg(const char *alg) { if (alg == NULL) return HX509_JWS_ALG_UNKNOWN; if (strcmp(alg, "RS256") == 0) return HX509_JWS_ALG_RS256; if (strcmp(alg, "RS384") == 0) return HX509_JWS_ALG_RS384; if (strcmp(alg, "RS512") == 0) return HX509_JWS_ALG_RS512; if (strcmp(alg, "ES256") == 0) return HX509_JWS_ALG_ES256; if (strcmp(alg, "ES384") == 0) return HX509_JWS_ALG_ES384; if (strcmp(alg, "ES512") == 0) return HX509_JWS_ALG_ES512; if (strcmp(alg, "EdDSA") == 0) return HX509_JWS_ALG_EDDSA; if (strcmp(alg, "none") == 0) return HX509_JWS_ALG_NONE; return HX509_JWS_ALG_UNKNOWN; } static int alg_is_rsa(hx509_jws_alg alg) { return alg == HX509_JWS_ALG_RS256 || alg == HX509_JWS_ALG_RS384 || alg == HX509_JWS_ALG_RS512; } static int alg_is_ecdsa(hx509_jws_alg alg) { return alg == HX509_JWS_ALG_ES256 || alg == HX509_JWS_ALG_ES384 || alg == HX509_JWS_ALG_ES512; } static int alg_is_eddsa(hx509_jws_alg alg) { return alg == HX509_JWS_ALG_EDDSA; } static const EVP_MD * alg_to_md(hx509_jws_alg alg) { switch (alg) { case HX509_JWS_ALG_RS256: case HX509_JWS_ALG_ES256: return EVP_sha256(); case HX509_JWS_ALG_RS384: case HX509_JWS_ALG_ES384: return EVP_sha384(); case HX509_JWS_ALG_RS512: case HX509_JWS_ALG_ES512: return EVP_sha512(); default: return NULL; } } /* ECDSA signature size for each algorithm */ static size_t ecdsa_sig_size(hx509_jws_alg alg) { switch (alg) { case HX509_JWS_ALG_ES256: return 64; /* 2 * 32 bytes */ case HX509_JWS_ALG_ES384: return 96; /* 2 * 48 bytes */ case HX509_JWS_ALG_ES512: return 132; /* 2 * 66 bytes */ default: return 0; } } static size_t ecdsa_coord_size(hx509_jws_alg alg) { switch (alg) { case HX509_JWS_ALG_ES256: return 32; case HX509_JWS_ALG_ES384: return 48; case HX509_JWS_ALG_ES512: return 66; default: return 0; } } /* * Convert ECDSA DER signature to JWS format (r || s) */ static unsigned char * ecdsa_der_to_jws(const unsigned char *der_sig, size_t der_len, hx509_jws_alg alg, size_t *out_len) { const unsigned char *p = der_sig; ECDSA_SIG *sig; const BIGNUM *r, *s; unsigned char *jws_sig; size_t coord_size = ecdsa_coord_size(alg); size_t sig_size = ecdsa_sig_size(alg); if (coord_size == 0) return NULL; sig = d2i_ECDSA_SIG(NULL, &p, der_len); if (sig == NULL) return NULL; ECDSA_SIG_get0(sig, &r, &s); jws_sig = calloc(1, sig_size); if (jws_sig == NULL) { ECDSA_SIG_free(sig); return NULL; } /* Pad r and s to fixed size, big-endian */ if (BN_bn2binpad(r, jws_sig, coord_size) < 0 || BN_bn2binpad(s, jws_sig + coord_size, coord_size) < 0) { ECDSA_SIG_free(sig); free(jws_sig); return NULL; } ECDSA_SIG_free(sig); *out_len = sig_size; return jws_sig; } /* * Convert JWS ECDSA signature (r || s) to DER format */ static unsigned char * ecdsa_jws_to_der(const unsigned char *jws_sig, size_t jws_len, hx509_jws_alg alg, size_t *out_len) { ECDSA_SIG *sig; BIGNUM *r, *s; unsigned char *der_sig = NULL; size_t coord_size = ecdsa_coord_size(alg); int der_len; if (coord_size == 0 || jws_len != ecdsa_sig_size(alg)) return NULL; r = BN_bin2bn(jws_sig, coord_size, NULL); s = BN_bin2bn(jws_sig + coord_size, coord_size, NULL); if (r == NULL || s == NULL) { BN_free(r); BN_free(s); return NULL; } sig = ECDSA_SIG_new(); if (sig == NULL) { BN_free(r); BN_free(s); return NULL; } /* ECDSA_SIG_set0 takes ownership of r and s */ if (ECDSA_SIG_set0(sig, r, s) != 1) { ECDSA_SIG_free(sig); BN_free(r); BN_free(s); return NULL; } der_len = i2d_ECDSA_SIG(sig, &der_sig); ECDSA_SIG_free(sig); if (der_len <= 0) { OPENSSL_free(der_sig); return NULL; } *out_len = der_len; return der_sig; } /* * Verify a JWS signature */ static int verify_signature(hx509_jws_alg alg, EVP_PKEY *pkey, const unsigned char *data, size_t data_len, const unsigned char *sig, size_t sig_len) { EVP_MD_CTX *mdctx = NULL; const EVP_MD *md; unsigned char *use_sig = NULL; size_t use_sig_len = sig_len; int ret = 0; mdctx = EVP_MD_CTX_new(); if (mdctx == NULL) return 0; if (alg_is_ecdsa(alg)) { /* Convert JWS signature format to DER */ use_sig = ecdsa_jws_to_der(sig, sig_len, alg, &use_sig_len); if (use_sig == NULL) goto out; } else { use_sig = (unsigned char *)rk_UNCONST(sig); use_sig_len = sig_len; } if (alg_is_eddsa(alg)) { /* EdDSA uses EVP_DigestVerify with NULL digest */ if (EVP_DigestVerifyInit(mdctx, NULL, NULL, NULL, pkey) != 1) goto out; if (EVP_DigestVerify(mdctx, use_sig, use_sig_len, data, data_len) == 1) ret = 1; } else { md = alg_to_md(alg); if (md == NULL) goto out; if (EVP_DigestVerifyInit(mdctx, NULL, md, NULL, pkey) != 1) goto out; if (EVP_DigestVerifyUpdate(mdctx, data, data_len) != 1) goto out; if (EVP_DigestVerifyFinal(mdctx, use_sig, use_sig_len) == 1) ret = 1; } out: EVP_MD_CTX_free(mdctx); if (alg_is_ecdsa(alg) && use_sig) OPENSSL_free(use_sig); return ret; } /* * Create a JWS signature */ static int create_signature(hx509_jws_alg alg, EVP_PKEY *pkey, const unsigned char *data, size_t data_len, unsigned char **sig_out, size_t *sig_len_out) { EVP_MD_CTX *mdctx = NULL; const EVP_MD *md; unsigned char *sig = NULL; size_t sig_len = 0; int ret = 0; mdctx = EVP_MD_CTX_new(); if (mdctx == NULL) return 0; if (alg_is_eddsa(alg)) { /* EdDSA uses EVP_DigestSign with NULL digest */ if (EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) != 1) goto out; /* Get required signature size */ if (EVP_DigestSign(mdctx, NULL, &sig_len, data, data_len) != 1) goto out; sig = malloc(sig_len); if (sig == NULL) goto out; if (EVP_DigestSign(mdctx, sig, &sig_len, data, data_len) != 1) goto out; *sig_out = sig; *sig_len_out = sig_len; sig = NULL; ret = 1; } else { unsigned char *der_sig = NULL; size_t der_len = 0; md = alg_to_md(alg); if (md == NULL) goto out; if (EVP_DigestSignInit(mdctx, NULL, md, NULL, pkey) != 1) goto out; if (EVP_DigestSignUpdate(mdctx, data, data_len) != 1) goto out; /* Get required signature size */ if (EVP_DigestSignFinal(mdctx, NULL, &der_len) != 1) goto out; der_sig = malloc(der_len); if (der_sig == NULL) goto out; if (EVP_DigestSignFinal(mdctx, der_sig, &der_len) != 1) { free(der_sig); goto out; } if (alg_is_ecdsa(alg)) { /* Convert DER signature to JWS format */ sig = ecdsa_der_to_jws(der_sig, der_len, alg, &sig_len); free(der_sig); if (sig == NULL) goto out; } else { sig = der_sig; sig_len = der_len; } *sig_out = sig; *sig_len_out = sig_len; sig = NULL; ret = 1; } out: EVP_MD_CTX_free(mdctx); free(sig); return ret; } /* * Load a public key from PEM data */ static EVP_PKEY * load_public_key_from_pem(const char *pem_data, size_t pem_len) { BIO *bio; EVP_PKEY *pkey = NULL; bio = BIO_new_mem_buf(pem_data, pem_len); if (bio == NULL) return NULL; pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); BIO_free(bio); return pkey; } /* * Load a private key from PEM data */ static EVP_PKEY * load_private_key_from_pem(const char *pem_data, size_t pem_len) { BIO *bio; EVP_PKEY *pkey = NULL; bio = BIO_new_mem_buf(pem_data, pem_len); if (bio == NULL) return NULL; pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); BIO_free(bio); return pkey; } /* * Check if key type matches algorithm */ static int key_matches_alg(EVP_PKEY *pkey, hx509_jws_alg alg) { int key_type = EVP_PKEY_base_id(pkey); if (alg_is_rsa(alg)) return key_type == EVP_PKEY_RSA || key_type == EVP_PKEY_RSA_PSS; if (alg_is_ecdsa(alg)) return key_type == EVP_PKEY_EC; if (alg_is_eddsa(alg)) return key_type == EVP_PKEY_ED25519 || key_type == EVP_PKEY_ED448; return 0; } /* * Public API */ /** * Verify a JWS (JSON Web Signature) compact serialization. * * @param context An hx509 context * @param token The JWS compact serialization (header.payload.signature) * @param pem_keys Array of PEM-encoded public keys to try * @param num_keys Number of keys in the array * @param payload_out If non-NULL, receives allocated payload data * @param payload_len_out If non-NULL, receives payload length * * @return 0 on success, error code otherwise */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_jws_verify(hx509_context context, const char *token, const char * const *pem_keys, size_t num_keys, void **payload_out, size_t *payload_len_out) { char *header_b64 = NULL, *payload_b64 = NULL, *sig_b64 = NULL; unsigned char *header_data = NULL, *sig_data = NULL; size_t header_len, sig_len; heim_object_t header_json = NULL; heim_string_t alg_str; const char *alg_name; hx509_jws_alg alg; const char *dot1, *dot2; size_t signing_input_len; int verified = 0; int ret = HX509_CRYPTO_SIG_INVALID_FORMAT; size_t i; if (payload_out) *payload_out = NULL; if (payload_len_out) *payload_len_out = 0; /* Parse compact serialization: header.payload.signature */ dot1 = strchr(token, '.'); if (dot1 == NULL) { hx509_set_error_string(context, 0, ret, "Invalid JWS format: missing first dot"); return ret; } dot2 = strchr(dot1 + 1, '.'); if (dot2 == NULL) { hx509_set_error_string(context, 0, ret, "Invalid JWS format: missing second dot"); return ret; } /* Extract parts */ header_b64 = strndup(token, dot1 - token); payload_b64 = strndup(dot1 + 1, dot2 - dot1 - 1); sig_b64 = strdup(dot2 + 1); if (header_b64 == NULL || payload_b64 == NULL || sig_b64 == NULL) { ret = ENOMEM; goto out; } /* Decode header */ header_data = base64url_decode(header_b64, &header_len); if (header_data == NULL) { hx509_set_error_string(context, 0, ret, "Invalid JWS: could not decode header"); goto out; } /* Parse header JSON */ header_json = heim_json_create_with_bytes((const char *)header_data, header_len, 10, 0, NULL); if (header_json == NULL) { hx509_set_error_string(context, 0, ret, "Invalid JWS: header is not valid JSON"); goto out; } if (heim_get_tid(header_json) != HEIM_TID_DICT) { hx509_set_error_string(context, 0, ret, "Invalid JWS: header is not a JSON object"); goto out; } /* Get algorithm */ alg_str = heim_dict_get_value(header_json, HSTR("alg")); if (alg_str == NULL || heim_get_tid(alg_str) != HEIM_TID_STRING) { hx509_set_error_string(context, 0, ret, "Invalid JWS: missing or invalid 'alg' header"); goto out; } alg_name = heim_string_get_utf8(alg_str); alg = parse_alg(alg_name); if (alg == HX509_JWS_ALG_UNKNOWN) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "Unsupported JWS algorithm: %s", alg_name); goto out; } if (alg == HX509_JWS_ALG_NONE) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "JWS 'none' algorithm not allowed"); goto out; } /* Decode signature */ sig_data = base64url_decode(sig_b64, &sig_len); if (sig_data == NULL) { hx509_set_error_string(context, 0, ret, "Invalid JWS: could not decode signature"); goto out; } /* Signing input is "header.payload" (the base64url-encoded parts) */ signing_input_len = dot2 - token; /* Try each key */ for (i = 0; i < num_keys && !verified; i++) { EVP_PKEY *pkey; if (pem_keys[i] == NULL) continue; pkey = load_public_key_from_pem(pem_keys[i], strlen(pem_keys[i])); if (pkey == NULL) continue; if (!key_matches_alg(pkey, alg)) { EVP_PKEY_free(pkey); continue; } if (verify_signature(alg, pkey, (const unsigned char *)token, signing_input_len, sig_data, sig_len)) { verified = 1; } EVP_PKEY_free(pkey); } if (!verified) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "JWS signature verification failed"); goto out; } /* Return payload if requested */ if (payload_out) { size_t payload_len; unsigned char *payload_data = base64url_decode(payload_b64, &payload_len); if (payload_data == NULL) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "Invalid JWS: could not decode payload"); goto out; } *payload_out = payload_data; if (payload_len_out) *payload_len_out = payload_len; } ret = 0; out: free(header_b64); free(payload_b64); free(sig_b64); free(header_data); free(sig_data); heim_release(header_json); return ret; } /** * Create a JWS (JSON Web Signature) compact serialization. * * @param context An hx509 context * @param alg_name Algorithm name ("RS256", "ES256", "EdDSA", etc.) * @param pem_private_key PEM-encoded private key * @param payload Payload data to sign * @param payload_len Length of payload * @param token_out Receives allocated JWS compact serialization * * @return 0 on success, error code otherwise */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_jws_sign(hx509_context context, const char *alg_name, const char *pem_private_key, const void *payload, size_t payload_len, char **token_out) { hx509_jws_alg alg; EVP_PKEY *pkey = NULL; heim_dict_t header = NULL; heim_string_t header_json_str = NULL; char *header_b64 = NULL, *payload_b64 = NULL, *sig_b64 = NULL; char *signing_input = NULL; unsigned char *sig = NULL; size_t sig_len = 0; int ret = HX509_CRYPTO_SIG_INVALID_FORMAT; *token_out = NULL; alg = parse_alg(alg_name); if (alg == HX509_JWS_ALG_UNKNOWN || alg == HX509_JWS_ALG_NONE) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "Unsupported or invalid JWS algorithm: %s", alg_name ? alg_name : "(null)"); goto out; } /* Load private key */ pkey = load_private_key_from_pem(pem_private_key, strlen(pem_private_key)); if (pkey == NULL) { hx509_set_error_string(context, 0, ret, "Could not load private key"); goto out; } if (!key_matches_alg(pkey, alg)) { hx509_set_error_string(context, 0, ret, "Key type does not match algorithm %s", alg_name); goto out; } /* Build header */ header = heim_dict_create(2); if (header == NULL) { ret = ENOMEM; goto out; } { heim_string_t s = heim_string_create(alg_name); heim_dict_set_value(header, HSTR("alg"), s); heim_release(s); } heim_dict_set_value(header, HSTR("typ"), HSTR("JWT")); /* Serialize header to JSON */ header_json_str = heim_json_copy_serialize(header, HEIM_JSON_F_ONE_LINE, NULL); if (header_json_str == NULL) { ret = ENOMEM; goto out; } /* Base64URL encode header and payload */ if (rk_base64url_encode(heim_string_get_utf8(header_json_str), strlen(heim_string_get_utf8(header_json_str)), &header_b64) < 0 || rk_base64url_encode(payload, payload_len, &payload_b64) < 0) { ret = ENOMEM; goto out; } /* Build signing input */ if (asprintf(&signing_input, "%s.%s", header_b64, payload_b64) < 0) { ret = ENOMEM; signing_input = NULL; goto out; } /* Create signature */ if (!create_signature(alg, pkey, (const unsigned char *)signing_input, strlen(signing_input), &sig, &sig_len)) { hx509_set_error_string(context, 0, ret, "Failed to create JWS signature"); goto out; } /* Base64URL encode signature */ if (rk_base64url_encode(sig, sig_len, &sig_b64) < 0) { ret = ENOMEM; goto out; } /* Build final token */ if (asprintf(token_out, "%s.%s", signing_input, sig_b64) < 0) { ret = ENOMEM; *token_out = NULL; goto out; } ret = 0; out: EVP_PKEY_free(pkey); heim_release(header); heim_release(header_json_str); free(header_b64); free(payload_b64); free(sig_b64); free(signing_input); free(sig); return ret; } /** * Create a signed JWS (JSON Web Signature) from payload using an hx509_private_key. * * This variant allows signing with keys from PKCS#11, PKCS#12, or other * hx509 keystore backends. * * @param context An hx509 context * @param alg_name Algorithm name ("RS256", "ES256", "EdDSA", etc.) * @param private_key An hx509_private_key containing the signing key * @param payload Data to sign * @param payload_len Length of payload * @param token_out Receives allocated JWS compact serialization * * @return 0 on success, error code otherwise */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_jws_sign_key(hx509_context context, const char *alg_name, hx509_private_key private_key, const void *payload, size_t payload_len, char **token_out) { hx509_jws_alg alg; EVP_PKEY *pkey; heim_dict_t header = NULL; heim_string_t header_json_str = NULL; char *header_b64 = NULL, *payload_b64 = NULL, *sig_b64 = NULL; char *signing_input = NULL; unsigned char *sig = NULL; size_t sig_len = 0; int ret = HX509_CRYPTO_SIG_INVALID_FORMAT; *token_out = NULL; if (private_key == NULL) { hx509_set_error_string(context, 0, ret, "No private key provided"); return ret; } alg = parse_alg(alg_name); if (alg == HX509_JWS_ALG_UNKNOWN || alg == HX509_JWS_ALG_NONE) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "Unsupported or invalid JWS algorithm: %s", alg_name ? alg_name : "(null)"); goto out; } /* Get EVP_PKEY from hx509_private_key - no need to free, owned by private_key */ pkey = private_key->private_key.pkey; if (pkey == NULL) { hx509_set_error_string(context, 0, ret, "Private key has no EVP_PKEY"); goto out; } if (!key_matches_alg(pkey, alg)) { hx509_set_error_string(context, 0, ret, "Key type does not match algorithm %s", alg_name); goto out; } /* Build header */ header = heim_dict_create(2); if (header == NULL) { ret = ENOMEM; goto out; } { heim_string_t s = heim_string_create(alg_name); heim_dict_set_value(header, HSTR("alg"), s); heim_release(s); } heim_dict_set_value(header, HSTR("typ"), HSTR("JWT")); /* Serialize header to JSON */ header_json_str = heim_json_copy_serialize(header, HEIM_JSON_F_ONE_LINE, NULL); if (header_json_str == NULL) { ret = ENOMEM; goto out; } /* Base64URL encode header and payload */ if (rk_base64url_encode(heim_string_get_utf8(header_json_str), strlen(heim_string_get_utf8(header_json_str)), &header_b64) < 0 || rk_base64url_encode(payload, payload_len, &payload_b64) < 0) { ret = ENOMEM; goto out; } /* Build signing input */ if (asprintf(&signing_input, "%s.%s", header_b64, payload_b64) < 0) { ret = ENOMEM; signing_input = NULL; goto out; } /* Create signature */ if (!create_signature(alg, pkey, (const unsigned char *)signing_input, strlen(signing_input), &sig, &sig_len)) { hx509_set_error_string(context, 0, ret, "Failed to create JWS signature"); goto out; } /* Base64URL encode signature */ if (rk_base64url_encode(sig, sig_len, &sig_b64) < 0) { ret = ENOMEM; goto out; } /* Build final token */ if (asprintf(token_out, "%s.%s", signing_input, sig_b64) < 0) { ret = ENOMEM; *token_out = NULL; goto out; } ret = 0; out: /* Note: pkey is NOT freed here - it's owned by private_key */ heim_release(header); heim_release(header_json_str); free(header_b64); free(payload_b64); free(sig_b64); free(signing_input); free(sig); return ret; } /** * Create a JWT (JSON Web Token) with standard claims using an hx509_private_key. * * This variant allows signing with keys from PKCS#11, PKCS#12, or other * hx509 keystore backends. * * @param context An hx509 context * @param alg_name Algorithm name ("RS256", "ES256", "EdDSA", etc.) * @param private_key An hx509_private_key containing the signing key * @param issuer Issuer claim (iss) * @param subject Subject claim (sub) * @param audience Audience claim (aud), may be NULL * @param lifetime Token lifetime in seconds from now * @param extra_claims Additional claims to include (may be NULL) * @param token_out Receives allocated JWT * * @return 0 on success, error code otherwise */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_jwt_sign_key(hx509_context context, const char *alg_name, hx509_private_key private_key, const char *issuer, const char *subject, const char *audience, time_t lifetime, heim_dict_t extra_claims, char **token_out) { heim_dict_t claims = NULL; heim_string_t claims_json = NULL; time_t now = time(NULL); int ret; *token_out = NULL; /* Build claims */ claims = heim_dict_create(10); if (claims == NULL) return ENOMEM; if (issuer) { heim_string_t s = heim_string_create(issuer); heim_dict_set_value(claims, HSTR("iss"), s); heim_release(s); } if (subject) { heim_string_t s = heim_string_create(subject); heim_dict_set_value(claims, HSTR("sub"), s); heim_release(s); } if (audience) { heim_string_t s = heim_string_create(audience); heim_dict_set_value(claims, HSTR("aud"), s); heim_release(s); } { heim_number_t n = heim_number_create(now); heim_dict_set_value(claims, HSTR("iat"), n); heim_release(n); n = heim_number_create(now + lifetime); heim_dict_set_value(claims, HSTR("exp"), n); heim_release(n); } /* Merge extra claims */ if (extra_claims) { /* TODO: iterate and copy extra claims */ } /* Serialize claims to JSON */ claims_json = heim_json_copy_serialize(claims, HEIM_JSON_F_ONE_LINE, NULL); if (claims_json == NULL) { ret = ENOMEM; goto out; } /* Create JWS */ ret = hx509_jws_sign_key(context, alg_name, private_key, heim_string_get_utf8(claims_json), strlen(heim_string_get_utf8(claims_json)), token_out); out: heim_release(claims); heim_release(claims_json); return ret; } /** * Verify a JWT (JSON Web Token) and extract claims. * * @param context An hx509 context * @param token The JWT compact serialization * @param pem_keys Array of PEM-encoded public keys to try * @param num_keys Number of keys in the array * @param required_aud Required audience (may be NULL to skip check) * @param time_now Current time (0 to use system time) * @param claims_out If non-NULL, receives claims as heim_dict_t (caller must release) * * @return 0 on success, error code otherwise */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_jwt_verify(hx509_context context, const char *token, const char * const *pem_keys, size_t num_keys, const char *required_aud, time_t time_now, heim_dict_t *claims_out) { void *payload = NULL; size_t payload_len = 0; heim_object_t claims = NULL; heim_number_t num; heim_string_t str; heim_object_t aud; int64_t exp_time, nbf_time; int ret; if (claims_out) *claims_out = NULL; if (time_now == 0) time_now = time(NULL); /* Verify signature and get payload */ ret = hx509_jws_verify(context, token, pem_keys, num_keys, &payload, &payload_len); if (ret) return ret; /* Parse claims JSON */ claims = heim_json_create_with_bytes(payload, payload_len, 10, 0, NULL); free(payload); if (claims == NULL) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "Invalid JWT: could not parse claims"); return ret; } if (heim_get_tid(claims) != HEIM_TID_DICT) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "Invalid JWT: claims is not a JSON object"); heim_release(claims); return ret; } /* Check expiration */ num = heim_dict_get_value(claims, HSTR("exp")); if (num && heim_get_tid(num) == HEIM_TID_NUMBER) { exp_time = heim_number_get_long(num); if (time_now > exp_time) { ret = HX509_CMS_SIGNER_NOT_FOUND; hx509_set_error_string(context, 0, ret, "JWT has expired"); heim_release(claims); return ret; } } /* Check not-before */ num = heim_dict_get_value(claims, HSTR("nbf")); if (num && heim_get_tid(num) == HEIM_TID_NUMBER) { nbf_time = heim_number_get_long(num); if (time_now < nbf_time) { ret = HX509_CMS_SIGNER_NOT_FOUND; hx509_set_error_string(context, 0, ret, "JWT not yet valid"); heim_release(claims); return ret; } } /* Check audience if required */ if (required_aud) { int found = 0; aud = heim_dict_get_value(claims, HSTR("aud")); if (aud == NULL) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "JWT missing required audience claim"); heim_release(claims); return ret; } if (heim_get_tid(aud) == HEIM_TID_STRING) { if (strcmp(heim_string_get_utf8((heim_string_t)aud), required_aud) == 0) found = 1; } else if (heim_get_tid(aud) == HEIM_TID_ARRAY) { size_t i, len = heim_array_get_length((heim_array_t)aud); for (i = 0; i < len && !found; i++) { str = heim_array_get_value((heim_array_t)aud, i); if (str && heim_get_tid(str) == HEIM_TID_STRING && strcmp(heim_string_get_utf8(str), required_aud) == 0) found = 1; } } if (!found) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "JWT audience does not match"); heim_release(claims); return ret; } } if (claims_out) *claims_out = (heim_dict_t)claims; else heim_release(claims); return 0; } /** * Create a JWT (JSON Web Token) with standard claims. * * @param context An hx509 context * @param alg_name Algorithm name ("RS256", "ES256", "EdDSA", etc.) * @param pem_private_key PEM-encoded private key * @param issuer Issuer claim (iss) * @param subject Subject claim (sub) * @param audience Audience claim (aud), may be NULL * @param lifetime Token lifetime in seconds from now * @param extra_claims Additional claims to include (may be NULL) * @param token_out Receives allocated JWT * * @return 0 on success, error code otherwise */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_jwt_sign(hx509_context context, const char *alg_name, const char *pem_private_key, const char *issuer, const char *subject, const char *audience, time_t lifetime, heim_dict_t extra_claims, char **token_out) { heim_dict_t claims = NULL; heim_string_t claims_json = NULL; time_t now = time(NULL); int ret; *token_out = NULL; /* Build claims */ claims = heim_dict_create(10); if (claims == NULL) return ENOMEM; if (issuer) { heim_string_t s = heim_string_create(issuer); heim_dict_set_value(claims, HSTR("iss"), s); heim_release(s); } if (subject) { heim_string_t s = heim_string_create(subject); heim_dict_set_value(claims, HSTR("sub"), s); heim_release(s); } if (audience) { heim_string_t s = heim_string_create(audience); heim_dict_set_value(claims, HSTR("aud"), s); heim_release(s); } { heim_number_t n = heim_number_create(now); heim_dict_set_value(claims, HSTR("iat"), n); heim_release(n); n = heim_number_create(now + lifetime); heim_dict_set_value(claims, HSTR("exp"), n); heim_release(n); } /* Merge extra claims */ if (extra_claims) { /* TODO: iterate and copy extra claims */ } /* Serialize claims to JSON */ claims_json = heim_json_copy_serialize(claims, HEIM_JSON_F_ONE_LINE, NULL); if (claims_json == NULL) { ret = ENOMEM; goto out; } /* Create JWS */ ret = hx509_jws_sign(context, alg_name, pem_private_key, heim_string_get_utf8(claims_json), strlen(heim_string_get_utf8(claims_json)), token_out); out: heim_release(claims); heim_release(claims_json); return ret; } /* * JWK (JSON Web Key) support */ /** * Convert a PEM-encoded public key to JWK format. * * @param context An hx509 context * @param pem_key PEM-encoded public key * @param jwk_out Receives JWK as heim_dict_t (caller must release) * * @return 0 on success, error code otherwise */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_pem_to_jwk(hx509_context context, const char *pem_key, heim_dict_t *jwk_out) { EVP_PKEY *pkey = NULL; heim_dict_t jwk = NULL; int key_type; int ret = HX509_CRYPTO_SIG_INVALID_FORMAT; *jwk_out = NULL; /* Try public key first, then private */ pkey = load_public_key_from_pem(pem_key, strlen(pem_key)); if (pkey == NULL) pkey = load_private_key_from_pem(pem_key, strlen(pem_key)); if (pkey == NULL) { hx509_set_error_string(context, 0, ret, "Could not load PEM key"); return ret; } jwk = heim_dict_create(10); if (jwk == NULL) { EVP_PKEY_free(pkey); return ENOMEM; } key_type = EVP_PKEY_base_id(pkey); if (key_type == EVP_PKEY_RSA || key_type == EVP_PKEY_RSA_PSS) { BIGNUM *n = NULL, *e = NULL; unsigned char *n_bin = NULL, *e_bin = NULL; char *n_b64 = NULL, *e_b64 = NULL; int n_len, e_len; heim_dict_set_value(jwk, HSTR("kty"), HSTR("RSA")); if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_N, &n) != 1 || EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_E, &e) != 1) { BN_free(n); BN_free(e); goto out_key; } n_len = BN_num_bytes(n); e_len = BN_num_bytes(e); n_bin = malloc(n_len); e_bin = malloc(e_len); if (n_bin == NULL || e_bin == NULL) { free(n_bin); free(e_bin); BN_free(n); BN_free(e); ret = ENOMEM; goto out_key; } BN_bn2bin(n, n_bin); BN_bn2bin(e, e_bin); if (rk_base64url_encode(n_bin, n_len, &n_b64) >= 0 && rk_base64url_encode(e_bin, e_len, &e_b64) >= 0) { heim_string_t s = heim_string_create(n_b64); heim_dict_set_value(jwk, HSTR("n"), s); heim_release(s); s = heim_string_create(e_b64); heim_dict_set_value(jwk, HSTR("e"), s); heim_release(s); ret = 0; } free(n_b64); free(e_b64); free(n_bin); free(e_bin); BN_free(n); BN_free(e); } else if (key_type == EVP_PKEY_EC) { BIGNUM *x = NULL, *y = NULL; unsigned char *x_bin = NULL, *y_bin = NULL; char *x_b64 = NULL, *y_b64 = NULL; char crv_name[64]; size_t crv_len; const char *crv = NULL; int coord_size = 0; heim_dict_set_value(jwk, HSTR("kty"), HSTR("EC")); if (EVP_PKEY_get_utf8_string_param(pkey, OSSL_PKEY_PARAM_GROUP_NAME, crv_name, sizeof(crv_name), &crv_len) != 1) goto out_key; /* Map OpenSSL curve name to JWK curve name */ if (strcmp(crv_name, "prime256v1") == 0 || strcmp(crv_name, "P-256") == 0) { crv = "P-256"; coord_size = 32; } else if (strcmp(crv_name, "secp384r1") == 0 || strcmp(crv_name, "P-384") == 0) { crv = "P-384"; coord_size = 48; } else if (strcmp(crv_name, "secp521r1") == 0 || strcmp(crv_name, "P-521") == 0) { crv = "P-521"; coord_size = 66; } else { hx509_set_error_string(context, 0, ret, "Unsupported EC curve: %s", crv_name); goto out_key; } { heim_string_t s = heim_string_create(crv); heim_dict_set_value(jwk, HSTR("crv"), s); heim_release(s); } if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_EC_PUB_X, &x) != 1 || EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_EC_PUB_Y, &y) != 1) { BN_free(x); BN_free(y); goto out_key; } x_bin = malloc(coord_size); y_bin = malloc(coord_size); if (x_bin == NULL || y_bin == NULL) { free(x_bin); free(y_bin); BN_free(x); BN_free(y); ret = ENOMEM; goto out_key; } if (BN_bn2binpad(x, x_bin, coord_size) < 0 || BN_bn2binpad(y, y_bin, coord_size) < 0) { free(x_bin); free(y_bin); BN_free(x); BN_free(y); goto out_key; } if (rk_base64url_encode(x_bin, coord_size, &x_b64) >= 0 && rk_base64url_encode(y_bin, coord_size, &y_b64) >= 0) { heim_string_t s = heim_string_create(x_b64); heim_dict_set_value(jwk, HSTR("x"), s); heim_release(s); s = heim_string_create(y_b64); heim_dict_set_value(jwk, HSTR("y"), s); heim_release(s); ret = 0; } free(x_b64); free(y_b64); free(x_bin); free(y_bin); BN_free(x); BN_free(y); } else if (key_type == EVP_PKEY_ED25519) { unsigned char pub_key[32]; size_t pub_len = sizeof(pub_key); char *x_b64 = NULL; heim_dict_set_value(jwk, HSTR("kty"), HSTR("OKP")); heim_dict_set_value(jwk, HSTR("crv"), HSTR("Ed25519")); if (EVP_PKEY_get_raw_public_key(pkey, pub_key, &pub_len) != 1) goto out_key; if (rk_base64url_encode(pub_key, pub_len, &x_b64) >= 0) { heim_string_t s = heim_string_create(x_b64); heim_dict_set_value(jwk, HSTR("x"), s); heim_release(s); free(x_b64); ret = 0; } } else if (key_type == EVP_PKEY_ED448) { unsigned char pub_key[57]; size_t pub_len = sizeof(pub_key); char *x_b64 = NULL; heim_dict_set_value(jwk, HSTR("kty"), HSTR("OKP")); heim_dict_set_value(jwk, HSTR("crv"), HSTR("Ed448")); if (EVP_PKEY_get_raw_public_key(pkey, pub_key, &pub_len) != 1) goto out_key; if (rk_base64url_encode(pub_key, pub_len, &x_b64) >= 0) { heim_string_t s = heim_string_create(x_b64); heim_dict_set_value(jwk, HSTR("x"), s); heim_release(s); free(x_b64); ret = 0; } } else { hx509_set_error_string(context, 0, ret, "Unsupported key type for JWK conversion"); } out_key: EVP_PKEY_free(pkey); if (ret == 0) { *jwk_out = jwk; } else { heim_release(jwk); } return ret; } /** * Serialize a JWK to JSON string. * * @param context An hx509 context * @param jwk JWK as heim_dict_t * @param json_out Receives allocated JSON string * * @return 0 on success, error code otherwise */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_jwk_to_json(hx509_context context, heim_dict_t jwk, char **json_out) { heim_string_t json_str; *json_out = NULL; json_str = heim_json_copy_serialize(jwk, HEIM_JSON_F_INDENT2, NULL); if (json_str == NULL) return ENOMEM; *json_out = strdup(heim_string_get_utf8(json_str)); heim_release(json_str); return *json_out ? 0 : ENOMEM; } /** * Convert a PEM-encoded key to JWK JSON string. * * @param context An hx509 context * @param pem_key PEM-encoded key * @param json_out Receives allocated JSON string * * @return 0 on success, error code otherwise */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_pem_to_jwk_json(hx509_context context, const char *pem_key, char **json_out) { heim_dict_t jwk = NULL; int ret; ret = hx509_pem_to_jwk(context, pem_key, &jwk); if (ret) return ret; ret = hx509_jwk_to_json(context, jwk, json_out); heim_release(jwk); return ret; } /* * JWK to EVP_PKEY conversion (reverse of hx509_pem_to_jwk) */ static EVP_PKEY * jwk_rsa_to_pkey(hx509_context context, heim_dict_t jwk) { EVP_PKEY *pkey = NULL; EVP_PKEY_CTX *pctx = NULL; OSSL_PARAM_BLD *bld = NULL; OSSL_PARAM *params = NULL; heim_string_t n_str, e_str; unsigned char *n_bin = NULL, *e_bin = NULL; size_t n_len = 0, e_len = 0; BIGNUM *n_bn = NULL, *e_bn = NULL; n_str = heim_dict_get_value(jwk, HSTR("n")); e_str = heim_dict_get_value(jwk, HSTR("e")); if (n_str == NULL || e_str == NULL || heim_get_tid(n_str) != HEIM_TID_STRING || heim_get_tid(e_str) != HEIM_TID_STRING) return NULL; n_bin = base64url_decode(heim_string_get_utf8(n_str), &n_len); e_bin = base64url_decode(heim_string_get_utf8(e_str), &e_len); if (n_bin == NULL || e_bin == NULL) goto out; n_bn = BN_bin2bn(n_bin, n_len, NULL); e_bn = BN_bin2bn(e_bin, e_len, NULL); if (n_bn == NULL || e_bn == NULL) goto out; bld = OSSL_PARAM_BLD_new(); if (bld == NULL) goto out; if (!OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_N, n_bn) || !OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_E, e_bn)) goto out; params = OSSL_PARAM_BLD_to_param(bld); if (params == NULL) goto out; pctx = EVP_PKEY_CTX_new_from_name(context->ossl->libctx, "RSA", context->ossl->propq); if (pctx == NULL) goto out; if (EVP_PKEY_fromdata_init(pctx) <= 0) goto out; if (EVP_PKEY_fromdata(pctx, &pkey, EVP_PKEY_PUBLIC_KEY, params) <= 0) pkey = NULL; out: EVP_PKEY_CTX_free(pctx); OSSL_PARAM_free(params); OSSL_PARAM_BLD_free(bld); BN_free(n_bn); BN_free(e_bn); free(n_bin); free(e_bin); return pkey; } static EVP_PKEY * jwk_ec_to_pkey(hx509_context context, heim_dict_t jwk) { EVP_PKEY *pkey = NULL; EVP_PKEY_CTX *pctx = NULL; OSSL_PARAM_BLD *bld = NULL; OSSL_PARAM *params = NULL; heim_string_t crv_str, x_str, y_str; const char *crv, *ossl_crv; unsigned char *x_bin = NULL, *y_bin = NULL; unsigned char *pub_bin = NULL; size_t x_len = 0, y_len = 0, coord_size = 0; crv_str = heim_dict_get_value(jwk, HSTR("crv")); x_str = heim_dict_get_value(jwk, HSTR("x")); y_str = heim_dict_get_value(jwk, HSTR("y")); if (crv_str == NULL || x_str == NULL || y_str == NULL || heim_get_tid(crv_str) != HEIM_TID_STRING || heim_get_tid(x_str) != HEIM_TID_STRING || heim_get_tid(y_str) != HEIM_TID_STRING) return NULL; crv = heim_string_get_utf8(crv_str); /* Map JWK curve name to OpenSSL name */ if (strcmp(crv, "P-256") == 0) { ossl_crv = "prime256v1"; coord_size = 32; } else if (strcmp(crv, "P-384") == 0) { ossl_crv = "secp384r1"; coord_size = 48; } else if (strcmp(crv, "P-521") == 0) { ossl_crv = "secp521r1"; coord_size = 66; } else { return NULL; } x_bin = base64url_decode(heim_string_get_utf8(x_str), &x_len); y_bin = base64url_decode(heim_string_get_utf8(y_str), &y_len); if (x_bin == NULL || y_bin == NULL) goto out; /* Build uncompressed point: 0x04 || x || y */ pub_bin = malloc(1 + coord_size * 2); if (pub_bin == NULL) goto out; pub_bin[0] = 0x04; /* Pad coordinates to fixed size if needed */ if (x_len <= coord_size) { memset(pub_bin + 1, 0, coord_size - x_len); memcpy(pub_bin + 1 + (coord_size - x_len), x_bin, x_len); } else { goto out; } if (y_len <= coord_size) { memset(pub_bin + 1 + coord_size, 0, coord_size - y_len); memcpy(pub_bin + 1 + coord_size + (coord_size - y_len), y_bin, y_len); } else { goto out; } bld = OSSL_PARAM_BLD_new(); if (bld == NULL) goto out; if (!OSSL_PARAM_BLD_push_utf8_string(bld, OSSL_PKEY_PARAM_GROUP_NAME, ossl_crv, 0) || !OSSL_PARAM_BLD_push_octet_string(bld, OSSL_PKEY_PARAM_PUB_KEY, pub_bin, 1 + coord_size * 2)) goto out; params = OSSL_PARAM_BLD_to_param(bld); if (params == NULL) goto out; pctx = EVP_PKEY_CTX_new_from_name(context->ossl->libctx, "EC", context->ossl->propq); if (pctx == NULL) goto out; if (EVP_PKEY_fromdata_init(pctx) <= 0) goto out; if (EVP_PKEY_fromdata(pctx, &pkey, EVP_PKEY_PUBLIC_KEY, params) <= 0) pkey = NULL; out: EVP_PKEY_CTX_free(pctx); OSSL_PARAM_free(params); OSSL_PARAM_BLD_free(bld); free(pub_bin); free(x_bin); free(y_bin); return pkey; } static EVP_PKEY * jwk_okp_to_pkey(hx509_context context, heim_dict_t jwk) { EVP_PKEY *pkey = NULL; heim_string_t crv_str, x_str; const char *crv; unsigned char *x_bin = NULL; size_t x_len = 0; crv_str = heim_dict_get_value(jwk, HSTR("crv")); x_str = heim_dict_get_value(jwk, HSTR("x")); if (crv_str == NULL || x_str == NULL || heim_get_tid(crv_str) != HEIM_TID_STRING || heim_get_tid(x_str) != HEIM_TID_STRING) return NULL; crv = heim_string_get_utf8(crv_str); /* crv is "Ed25519" or "Ed448", which OpenSSL accepts as key type names */ if (strcmp(crv, "Ed25519") != 0 && strcmp(crv, "Ed448") != 0) return NULL; x_bin = base64url_decode(heim_string_get_utf8(x_str), &x_len); if (x_bin == NULL) return NULL; pkey = EVP_PKEY_new_raw_public_key_ex(context->ossl->libctx, crv, context->ossl->propq, x_bin, x_len); free(x_bin); return pkey; } static EVP_PKEY * jwk_to_pkey(hx509_context context, heim_dict_t jwk) { heim_string_t kty_str; const char *kty; kty_str = heim_dict_get_value(jwk, HSTR("kty")); if (kty_str == NULL || heim_get_tid(kty_str) != HEIM_TID_STRING) return NULL; kty = heim_string_get_utf8(kty_str); if (strcmp(kty, "RSA") == 0) return jwk_rsa_to_pkey(context, jwk); else if (strcmp(kty, "EC") == 0) return jwk_ec_to_pkey(context, jwk); else if (strcmp(kty, "OKP") == 0) return jwk_okp_to_pkey(context, jwk); return NULL; } /** * Verify a JWS using JWK or JWKS for public keys. * * @param context An hx509 context * @param token The JWS compact serialization (header.payload.signature) * @param jwk_json JWK or JWKS JSON string containing public key(s) * @param payload_out If non-NULL, receives allocated payload data * @param payload_len_out If non-NULL, receives payload length * * @return 0 on success, error code otherwise */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_jws_verify_jwk(hx509_context context, const char *token, const char *jwk_json, void **payload_out, size_t *payload_len_out) { char *header_b64 = NULL, *payload_b64 = NULL, *sig_b64 = NULL; unsigned char *header_data = NULL, *sig_data = NULL; size_t header_len, sig_len; heim_object_t header_json = NULL; heim_object_t jwk_obj = NULL; heim_string_t alg_str; const char *alg_name; hx509_jws_alg alg; const char *dot1, *dot2; size_t signing_input_len; EVP_PKEY **pkeys = NULL; size_t num_keys = 0; int verified = 0; int ret = HX509_CRYPTO_SIG_INVALID_FORMAT; size_t i; if (payload_out) *payload_out = NULL; if (payload_len_out) *payload_len_out = 0; /* Parse JWK or JWKS JSON */ jwk_obj = heim_json_create(jwk_json, 10, 0, NULL); if (jwk_obj == NULL) { hx509_set_error_string(context, 0, ret, "Invalid JWK/JWKS JSON"); return ret; } /* Determine if single JWK or JWKS */ if (heim_get_tid(jwk_obj) == HEIM_TID_DICT) { heim_object_t keys_array = heim_dict_get_value(jwk_obj, HSTR("keys")); if (keys_array != NULL && heim_get_tid(keys_array) == HEIM_TID_ARRAY) { /* JWKS format: {"keys": [...]} */ heim_array_t arr = (heim_array_t)keys_array; num_keys = heim_array_get_length(arr); pkeys = calloc(num_keys, sizeof(EVP_PKEY *)); if (pkeys == NULL) { ret = ENOMEM; goto out; } for (i = 0; i < num_keys; i++) { heim_dict_t k = (heim_dict_t)heim_array_get_value(arr, i); if (k && heim_get_tid(k) == HEIM_TID_DICT) pkeys[i] = jwk_to_pkey(context, k); } } else { /* Single JWK format */ num_keys = 1; pkeys = calloc(1, sizeof(EVP_PKEY *)); if (pkeys == NULL) { ret = ENOMEM; goto out; } pkeys[0] = jwk_to_pkey(context, (heim_dict_t)jwk_obj); } } else { hx509_set_error_string(context, 0, ret, "Invalid JWK/JWKS: expected JSON object"); goto out; } /* Parse compact serialization: header.payload.signature */ dot1 = strchr(token, '.'); if (dot1 == NULL) { hx509_set_error_string(context, 0, ret, "Invalid JWS format: missing first dot"); goto out; } dot2 = strchr(dot1 + 1, '.'); if (dot2 == NULL) { hx509_set_error_string(context, 0, ret, "Invalid JWS format: missing second dot"); goto out; } /* Extract parts */ header_b64 = strndup(token, dot1 - token); payload_b64 = strndup(dot1 + 1, dot2 - dot1 - 1); sig_b64 = strdup(dot2 + 1); if (header_b64 == NULL || payload_b64 == NULL || sig_b64 == NULL) { ret = ENOMEM; goto out; } /* Decode header */ header_data = base64url_decode(header_b64, &header_len); if (header_data == NULL) { hx509_set_error_string(context, 0, ret, "Invalid JWS: could not decode header"); goto out; } /* Parse header JSON */ header_json = heim_json_create_with_bytes((const char *)header_data, header_len, 10, 0, NULL); if (header_json == NULL) { hx509_set_error_string(context, 0, ret, "Invalid JWS: header is not valid JSON"); goto out; } if (heim_get_tid(header_json) != HEIM_TID_DICT) { hx509_set_error_string(context, 0, ret, "Invalid JWS: header is not a JSON object"); goto out; } /* Get algorithm */ alg_str = heim_dict_get_value(header_json, HSTR("alg")); if (alg_str == NULL || heim_get_tid(alg_str) != HEIM_TID_STRING) { hx509_set_error_string(context, 0, ret, "Invalid JWS: missing or invalid 'alg' header"); goto out; } alg_name = heim_string_get_utf8(alg_str); alg = parse_alg(alg_name); if (alg == HX509_JWS_ALG_UNKNOWN) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "Unsupported JWS algorithm: %s", alg_name); goto out; } if (alg == HX509_JWS_ALG_NONE) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "JWS 'none' algorithm not allowed"); goto out; } /* Decode signature */ sig_data = base64url_decode(sig_b64, &sig_len); if (sig_data == NULL) { hx509_set_error_string(context, 0, ret, "Invalid JWS: could not decode signature"); goto out; } /* Signing input is "header.payload" */ signing_input_len = dot2 - token; /* Try each key */ for (i = 0; i < num_keys && !verified; i++) { if (pkeys[i] == NULL) continue; if (!key_matches_alg(pkeys[i], alg)) continue; if (verify_signature(alg, pkeys[i], (const unsigned char *)token, signing_input_len, sig_data, sig_len)) { verified = 1; } } if (!verified) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "JWS signature verification failed"); goto out; } /* Return payload if requested */ if (payload_out) { size_t payload_len; unsigned char *payload_data = base64url_decode(payload_b64, &payload_len); if (payload_data == NULL) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "Invalid JWS: could not decode payload"); goto out; } *payload_out = payload_data; if (payload_len_out) *payload_len_out = payload_len; } ret = 0; out: free(header_b64); free(payload_b64); free(sig_b64); free(header_data); free(sig_data); heim_release(header_json); heim_release(jwk_obj); if (pkeys) { for (i = 0; i < num_keys; i++) EVP_PKEY_free(pkeys[i]); free(pkeys); } return ret; } /** * Verify a JWT using JWK or JWKS for public keys. * * @param context An hx509 context * @param token The JWT compact serialization * @param jwk_json JWK or JWKS JSON string containing public key(s) * @param required_aud Required audience (may be NULL to skip check) * @param time_now Current time (0 to use system time) * @param claims_out If non-NULL, receives claims as heim_dict_t (caller must release) * * @return 0 on success, error code otherwise */ HX509_LIB_FUNCTION int HX509_LIB_CALL hx509_jwt_verify_jwk(hx509_context context, const char *token, const char *jwk_json, const char *required_aud, time_t time_now, heim_dict_t *claims_out) { void *payload = NULL; size_t payload_len = 0; heim_object_t claims = NULL; heim_number_t num; heim_string_t str; heim_object_t aud; int64_t exp_time, nbf_time; int ret; if (claims_out) *claims_out = NULL; if (time_now == 0) time_now = time(NULL); /* Verify signature and get payload */ ret = hx509_jws_verify_jwk(context, token, jwk_json, &payload, &payload_len); if (ret) return ret; /* Parse claims JSON */ claims = heim_json_create_with_bytes(payload, payload_len, 10, 0, NULL); free(payload); if (claims == NULL) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "Invalid JWT: could not parse claims"); return ret; } if (heim_get_tid(claims) != HEIM_TID_DICT) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "Invalid JWT: claims is not a JSON object"); heim_release(claims); return ret; } /* Check expiration */ num = heim_dict_get_value(claims, HSTR("exp")); if (num && heim_get_tid(num) == HEIM_TID_NUMBER) { exp_time = heim_number_get_long(num); if (time_now > exp_time) { ret = HX509_CMS_SIGNER_NOT_FOUND; hx509_set_error_string(context, 0, ret, "JWT has expired"); heim_release(claims); return ret; } } /* Check not-before */ num = heim_dict_get_value(claims, HSTR("nbf")); if (num && heim_get_tid(num) == HEIM_TID_NUMBER) { nbf_time = heim_number_get_long(num); if (time_now < nbf_time) { ret = HX509_CMS_SIGNER_NOT_FOUND; hx509_set_error_string(context, 0, ret, "JWT not yet valid"); heim_release(claims); return ret; } } /* Check audience if required */ if (required_aud) { int found = 0; aud = heim_dict_get_value(claims, HSTR("aud")); if (aud == NULL) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "JWT missing required audience claim"); heim_release(claims); return ret; } if (heim_get_tid(aud) == HEIM_TID_STRING) { if (strcmp(heim_string_get_utf8((heim_string_t)aud), required_aud) == 0) found = 1; } else if (heim_get_tid(aud) == HEIM_TID_ARRAY) { size_t i, len = heim_array_get_length((heim_array_t)aud); for (i = 0; i < len && !found; i++) { str = heim_array_get_value((heim_array_t)aud, i); if (str && heim_get_tid(str) == HEIM_TID_STRING && strcmp(heim_string_get_utf8(str), required_aud) == 0) found = 1; } } if (!found) { ret = HX509_CRYPTO_SIG_INVALID_FORMAT; hx509_set_error_string(context, 0, ret, "JWT audience does not match"); heim_release(claims); return ret; } } if (claims_out) *claims_out = (heim_dict_t)claims; else heim_release(claims); return 0; }