JWT ES256 token using ECDSA

I’m trying to create ES256 JWT using keys generated with openssl, my example works fine with RS256 keys - jwt.io verifies generated RS256 token, but not ES256. Here are code parts for RS256 generation:

mbedtls_pk_context pk_context;
mbedtls_pk_init(&pk_context);

mbedtls_pk_parse_key(
    &pk_context,        // The PK context to fill
    private_key,        // Input buffer to parse (must contain a null-terminated string)
    private_key_len,    // Size of the key in bytes (includes the terminating null byte)
    NULL,               // Optional password for decryption (if key was encrypted during generation)
    0);                 // Size of the password in bytes

mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_ctr_drbg_init(&ctr_drbg);

mbedtls_entropy_context entropy;
mbedtls_entropy_init(&entropy);

mbedtls_ctr_drbg_seed(
    &ctr_drbg,              // The CTR_DRBG context to seed
    mbedtls_entropy_func,   // The entropy callback
    &entropy,               // The entropy context to pass to entropy callback
    NULL,                   // The personalization string
    0);                     // The length of the personalization string

uint8_t md_checksum[32];
mbedtls_md(
    mbedtls_md_info_from_type(MBEDTLS_MD_SHA256),   // MD algorithm
    base64_header_and_payload,                      // Input data buffer
    strlen((char*)base64_header_and_payload),       // Input data length
    md_checksum);                                   // Output MD checksum

uint8_t signature[250];
size_t signature_len;
mbedtls_pk_sign(
    &pk_context,                // PK context to use
    MBEDTLS_MD_SHA256,          // Hash algorithm used
    md_checksum,                // Hash of the message to sign
    sizeof(md_checksum),        // Hash length
    signature,                  // Place to write the signature
    &signature_len,             // Number of bytes written
    mbedtls_ctr_drbg_random,    // RNG function
    &ctr_drbg);                 // RNG parameter

char base64_signature[250];
size_t base64_signature_len;
base64url_encode(
    (const void *)signature,    // Data to encode.
    signature_len,              // Length of data to encode.
    base64_signature,           // Base64 encoded data.
    &base64_signature_len);     // Length of base64 encoded data.

char* token = (char*)malloc(strlen((char*)base64_header_and_payload) + 1 + strlen((char*)base64_signature) + 1);
sprintf(token, "%s.%s", base64_header_and_payload, base64_signature);

For ES256 I generated pair of keys with:

openssl ecparam -name secp256r1 -genkey -noout -out ecdsa_private_key.pem
openssl ec -in ecdsa_private_key.pem -pubout -out ecdsa_public_key.pem

mbedtls_pk_get_type() added after mbedtls_pk_parse_key() returns MBEDTLS_PK_ECKEY. I’m not shure at this point that it’s correct, because there is also MBEDTLS_PK_ECDSA type, maybe I somehow need to get this one in return?

mbedtls_pk_type_t pk_type;
pk_type = mbedtls_pk_get_type(&pk_context);

Also was trying to work with ecdsa_context, added this part after mbedtls_pk_parse_key():

mbedtls_ecdsa_context ecdsa_context;
mbedtls_ecdsa_init(&ecdsa_context);
mbedtls_ecdsa_from_keypair(&ecdsa_context, pk_context.pk_ctx);

And this function instead of mbedtls_pk_sign():

mbedtls_ecdsa_write_signature(
    &ecdsa_context,             // PK context to use
    MBEDTLS_MD_SHA256,          // Hash algorithm used
    md_checksum,                // Hash of the message to sign
    sizeof(md_checksum),        // Hash length
    signature,                  // Place to write the signature
    &signature_len,             // Number of bytes written
    mbedtls_ctr_drbg_random,    // RNG function
    &ctr_drbg);                 // RNG parameter

It generates token in both cases, but I can’t validate it in jwt.io. Please suggest the correct way of ES256 generation with already existing private and public keys.

Tried generated EC keys, got same result:

mbedtls_pk_setup(
    &pk_context, 
    mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)); 

mbedtls_ecp_gen_key(
    MBEDTLS_ECP_DP_SECP256R1,
    mbedtls_pk_ec(pk_context),
    mbedtls_ctr_drbg_random,
    &ctr_drbg);

mbedtls_pk_sign(
    &pk_context,                // PK context to use
    MBEDTLS_MD_SHA256,          // Hash algorithm used
    md_checksum,                // Hash of the message to sign
    sizeof(md_checksum),        // Hash length
    signature,                  // Place to write the signature
    &signature_len,             // Number of bytes written
    mbedtls_ctr_drbg_random,    // RNG function
    &ctr_drbg);                 // RNG parameter

// Get public key to verify on jwt web site
mbedtls_pk_write_pubkey_pem(pk_context, output_buf, 500);

And ECDSA generated keys:

mbedtls_ecdsa_genkey(
    &ecdsa_context,
    MBEDTLS_ECP_DP_SECP256R1,
    mbedtls_ctr_drbg_random,
    &ctr_drbg);

mbedtls_ecdsa_write_signature(
    &ecdsa_context,             // ECDSA context to use
    MBEDTLS_MD_SHA256,          // Hash algorithm used
    md_checksum,                // Hash of the message to sign
    sizeof(md_checksum),        // Hash length
    signature,                  // Place to write the signature
    &signature_len,             // Number of bytes written
    mbedtls_ctr_drbg_random,    // RNG function
    &ctr_drbg);                 // RNG parameter

// Don't know how to get public key from ECDSA context, so I'm getting X, Y points to create JWK and convert it to PEM
mbedtls_ecp_point_write_binary(&key->grp, &key->Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &len, buf, sizeof buf )

Key from the buffer:
044EAE13BB1A62771AB9E1238FAA1F71A40B9AF87BC8004999E1BD1EF56470D66F0BB8B9C7774A56AD79F94BE7C466D9BA7640C0C0D9E802B494EAF042702588EC

04 - Uncompressed Public Key
0BB8B9C7774A56AD79F94BE7C466D9BA7640C0C0D9E802B494EAF042702588EC - Public Key X coord
528D77BE738FB00C96644F07F2FDAE7CFA9EEE6F6A6E0544CDFF548724D82DD4 - Public Key Y coord

JWK with these HEX values encoded with Base64:

{
"kty": "EC",
"use": "sig",
"crv": "P-256",
"x": "C7i5x3dKVq15+UvnxGbZunZAwMDZ6AK0lOrwQnAliOw=",
"y": "Uo13vnOPsAyWZE8H8v2ufPqe7m9qbgVEzf9UhyTYLdQ=",
"alg": "ES256"
}

PEM key:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC7i5x3dKVq15+UvnxGbZunZAwMDZ
6AK0lOrwQnAliOxSjXe+c4+wDJZkTwfy/a58+p7ub2puBUTN/1SHJNgt1A==
-----END PUBLIC KEY-----

Still can’t verify in jwt.io

Edited: I’m not 100% sure that last part with JWK creation from ECDSA hex key is correct, because I coudn’t verify generated JWT with PEM key created from that JWK after decoding ASN.1 signature.

Further investigation led me to github issue that was related to mine one:

To be verified in jwt.io signature must be 64 bytes, but mbedtls generates 70 bytes ASN.1 encoded I suppose.

Openssl generated key pair:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErbJbQ9/uCBv+76EkuxzvzSHRNrV+
EM+xpJQXT3eI/d2ilvFM/OvTgWPU3UotDA63Snt+uZaow3YQcihzDhgsRw==
-----END PUBLIC KEY-----

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIHGydA+QgPlivuLor6T+6oJs+Wf3thxgeegVF/IWqnQsoAoGCCqGSM49
AwEHoUQDQgAErbJbQ9/uCBv+76EkuxzvzSHRNrV+EM+xpJQXT3eI/d2ilvFM/OvT
gWPU3UotDA63Snt+uZaow3YQcihzDhgsRw==
-----END EC PRIVATE KEY-----

JWT signed with mbedtls:

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjUxLCJleHAiOjM2NTEsImlzcyI6ImVzcDMyIn0.MEQCICQFiUNe1WmuUUOti5YtVpKyg7vebG8JCNTbi-xBNzKYAiBQmZ5BwhbIw2xbiSSjOIfHHP-Oer4SzvrpvHziN1SFxw

Base64url signature part decoded through Hex to Base64: Encode and decode bytes online - cryptii (RFC 4648 $5) is 70 bytes:

30 44 02 20 24 05 89 43 5e d5 69 ae 51 43 ad 8b 96 2d 56 92 b2 83 bb de 6c 6f 09 08 d4 db 8b ec 41 37 32 98 02 20 50 99 9e 41 c2 16 c8 c3 6c 5b 89 24 a3 38 87 c7 1c ff 8e 7a be 12 ce fa e9 bc 7c e2 37 54 85 c7

https://lapo.it/asn1js/ parses it correctly into two integers 32 bytes each:

24 05 89 43 5e d5 69 ae 51 43 ad 8b 96 2d 56 92 b2 83 bb de 6c 6f 09 08 d4 db 8b ec 41 37 32 98

50 99 9e 41 c2 16 c8 c3 6c 5b 89 24 a3 38 87 c7 1c ff 8e 7a be 12 ce fa e9 bc 7c e2 37 54 85 c7

Concatenated and base64url encoded:

JAWJQ17Vaa5RQ62Lli1WkrKDu95sbwkI1NuL7EE3MphQmZ5BwhbIw2xbiSSjOIfHHP-Oer4SzvrpvHziN1SFxw

With this signature JWT is correctly verified in jwt.io.