为何mbedtls生成的ECDSA签名无法在JOSE中验证(RSA正常)
ECDSA签名JWT外部验证失败问题
我在ESP32开发板上用Arduino IDE和配套mbedtls库开发了一个签发、验证JWT的应用。原本用RSA签名一切正常,换成ECDSA(prime256v1+SHA256,对应ES256算法)后,应用内可以正常签发和验证令牌,但用JOSE库或jwt.io调试器等外部工具验证时全部失败。
示例令牌:
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NzI0MTMxNjgsImV4cCI6MTY3MjQxNjc2OH0.MEUCIAjwEDXI424qjrAkSzZ_ydcVLOSAvfQ8YVddYvzDzMvQAiEAkVy4d-hZ01KpcMNKhPHk8E_SDYiB4JKwhm-Kc-Z81rI
对应公钥:
-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUGnNIOhPhZZSOg4A4BqAFtGO13W4BGDQpQ0ieTvLU9/CXrY7W77o7pNx7tvugeIoYJxS0NjmxvT4TMpo4Z8P7A== -----END PUBLIC KEY-----
密钥生成命令:
openssl ecparam -name prime256v1 -genkey -noout -out ecc-private.pem openssl ec -in ecc-private.pem -pubout -out ecc-public.pem
加载私钥代码:
//get the key byte *keybuffer = (byte*)malloc((ecc_priv.length()+1)*sizeof(byte)); ecc_priv.getBytes(keybuffer, ecc_priv.length() + 1); mbedtls_pk_context pk_context; mbedtls_pk_init(&pk_context); int rc = mbedtls_pk_parse_key(&pk_context, keybuffer, ecc_priv.length() + 1, NULL, 0); if (rc != 0){ printf("Failed to mbedtls_pk_parse_key: %d (-0x%x): %s\n", rc, -rc, mbedtlsError(rc)); return -1; } free(keybuffer);
签名核心代码:
//mbedtls context mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; mbedtls_ctr_drbg_init(&ctr_drbg); mbedtls_entropy_init(&entropy); const char* pers="some entropy"; mbedtls_ctr_drbg_seed( &ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char*)pers, strlen(pers)); //get the header and payload bytes byte *headerAndPayloadbytes = (byte*)malloc((headerAndPayload.length()+1)*sizeof(byte)); headerAndPayload.getBytes(headerAndPayloadbytes, headerAndPayload.length() + 1); //prepare digest uint8_t digest[32]; rc = mbedtls_md(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), headerAndPayloadbytes, headerAndPayload.length(), digest); if (rc != 0) { printf("Failed to mbedtls_md: %d (-0x%x): %s\n", rc, -rc, mbedtlsError(rc)); return -1; } free(headerAndPayloadbytes); //prepare output byte *oBuf = (byte*)malloc(5000*sizeof(byte)); size_t retSize; //sign digest rc = mbedtls_pk_sign(&pk_context, MBEDTLS_MD_SHA256, digest, sizeof(digest), oBuf, &retSize, mbedtls_ctr_drbg_random, &ctr_drbg); if (rc != 0) { printf("Failed to mbedtls_pk_sign: %d (-0x%x): %s\n", rc, -rc, mbedtlsError(rc)); return -1; } //encode signature to base64 unsigned int osize = encode_base64_length(retSize); byte *output = (byte*)malloc((osize+1)*sizeof(byte)); encode_base64(oBuf, retSize, output); String sig = String((char*)output); free(output); //base64 URL specific sig.replace('+','-'); sig.replace('/','_'); sig.replace("=",""); String completejwt = headerAndPayload + "." + sig; //free resources mbedtls_ctr_drbg_free( &ctr_drbg ); mbedtls_entropy_free( &entropy ); mbedtls_pk_free(&pk_context); free(oBuf);
问题原因与解决方案
核心问题
mbedtls的mbedtls_pk_sign函数对ECDSA签名返回的是ASN.1 DER编码格式,而JWT的ES256算法要求签名必须是raw格式(32字节r值拼接32字节s值)。这就是为什么应用内验证正常(验证时用了相同的mbedtls逻辑,自动处理了DER格式),但外部工具验证失败的原因。
解决步骤
需要将mbedtls生成的DER格式签名转换为JWT要求的raw格式,具体代码修改如下:
- 添加DER解析函数,从DER编码中提取r和s:
int ecdsa_der_to_raw(const unsigned char *der_signature, size_t der_len, unsigned char *raw_signature) { if (der_signature[0] != 0x30 || der_signature[1] != der_len - 2) { return -1; } // 提取r size_t r_len = der_signature[3]; const unsigned char *r_data = der_signature + 4; // 提取s size_t s_len = der_signature[4 + r_len + 1]; const unsigned char *s_data = der_signature + 4 + r_len + 2; // 确保r和s都是32字节(prime256v1的长度),不足的话前面补0 memset(raw_signature, 0, 32); memcpy(raw_signature + (32 - r_len), r_data, r_len); memset(raw_signature + 32, 0, 32); memcpy(raw_signature + 32 + (32 - s_len), s_data, s_len); return 0; }
- 修改签名后的处理逻辑,替换原有的Base64编码部分:
// 原签名代码到rc = mbedtls_pk_sign(...)之后 if (rc != 0) { printf("Failed to mbedtls_pk_sign: %d (-0x%x): %s\n", rc, -rc, mbedtlsError(rc)); return -1; } // 新增:将DER签名转换为raw格式 unsigned char raw_sig[64]; rc = ecdsa_der_to_raw(oBuf, retSize, raw_sig); if (rc != 0) { printf("Failed to convert DER to raw signature\n"); return -1; } // 对raw签名进行Base64URL编码 unsigned int osize = encode_base64_length(64); byte *output = (byte*)malloc((osize+1)*sizeof(byte)); encode_base64(raw_sig, 64, output); String sig = String((char*)output); free(output); // 后续的Base64URL替换逻辑不变 sig.replace('+','-'); sig.replace('/','_'); sig.replace("=",""); String completejwt = headerAndPayload + "." + sig;
验证修改效果
将转换后的raw签名进行Base64URL编码后,生成的JWT就能在jwt.io等工具中正常验证。比如你提供的示例令牌,把签名部分转换为raw格式后,对应的Base64URL编码为AjwEDXI424qjrAkSzZ_ydcVLOSAvfQ8YVddYvzDzMvQkVy4d-hZ01KpcMNKhPHk8E_SDYiB4JKwhm-Kc-Z81rI,替换原签名后就能通过外部验证。
内容的提问来源于stack exchange,提问作者mkuhlmann
相关产品推荐
相关产品推荐

