/*
 * 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 <heimbase.h>
#include <base64.h>

#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/core_names.h>
#include <openssl/param_build.h>
#include <openssl/ec.h>

/* 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;
}
