JWK(JSON Web Key)是JSON格式表示的密钥规范,在OAuth2、JWT验签等场景中经常被用来传递椭圆曲线公钥。椭圆曲线公钥由两个坐标点x和y组成,将其编码为JWK格式需要遵循固定的规则,同时需要注意多个容易出错的细节。

JWK椭圆曲线公钥的标准结构
符合RFC7517规范的椭圆曲线公钥JWK至少包含以下几个字段:
- kty:密钥类型,椭圆曲线公钥固定为
EC - crv:曲线标识,比如P-256、P-384、secp256k1等
- x:椭圆曲线公钥的x坐标,经过Base64URL编码的字符串
- y:椭圆曲线公钥的y坐标,经过Base64URL编码的字符串
- use:可选字段,表示密钥用途,比如
sig表示用于签名
一个标准的P-256曲线JWK公钥示例如下:
{
"kty": "EC",
"crv": "P-256",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use": "sig"
}
坐标编码的完整流程
1. 获取原始坐标字节
椭圆曲线公钥的x和y坐标都是大整数,首先需要将这两个整数转换为固定长度的字节数组。不同曲线的坐标字节长度不同,比如P-256曲线的坐标长度是32字节,P-384是48字节,转换时需要在高位补0到指定长度。
2. Base64URL编码
将x和y的字节数组分别进行Base64URL编码,需要注意Base64URL和常规Base64的区别:
- 不使用填充字符
= - 将
+替换为- - 将
/替换为_
3. 组装JWK对象
将编码后的x、y字符串和对应的kty、crv等字段组装成JSON对象,就得到了最终的JWK公钥。
下面是用Python实现坐标编码的示例代码:
import base64
import json
import hashlib
def int_to_bytes_padded(num, length):
# 将整数转换为固定长度的字节数组,高位补0
raw_bytes = num.to_bytes((num.bit_length() + 7) // 8, byteorder='big')
if len(raw_bytes) < length:
return b'x00' * (length - len(raw_bytes)) + raw_bytes
return raw_bytes
def base64url_encode(data):
# 实现Base64URL编码,去掉填充,替换特殊字符
encoded = base64.urlsafe_b64encode(data)
return encoded.rstrip(b'=').decode('utf-8')
def ec_pubkey_to_jwk(x_int, y_int, crv):
# 不同曲线对应的坐标字节长度
crv_length_map = {
'P-256': 32,
'P-384': 48,
'P-521': 66
}
if crv not in crv_length_map:
raise ValueError(f"不支持的曲线: {crv}")
length = crv_length_map[crv]
# 转换x和y为字节数组
x_bytes = int_to_bytes_padded(x_int, length)
y_bytes = int_to_bytes_padded(y_int, length)
# Base64URL编码
x_b64 = base64url_encode(x_bytes)
y_b64 = base64url_encode(y_bytes)
# 组装JWK
jwk = {
"kty": "EC",
"crv": crv,
"x": x_b64,
"y": y_b64,
"use": "sig"
}
return json.dumps(jwk, indent=2)
# 示例:假设有两个P-256曲线的坐标整数
x_example = 1234567890123456789012345678901234567890123456789012345678901234
y_example = 9876543210987654321098765432109876543210987654321098765432109876
print(ec_pubkey_to_jwk(x_example, y_example, "P-256"))
常见编码陷阱
1. 坐标字节长度补位错误
很多开发者直接将整数转换为字节就进行编码,没有补到曲线要求的长度。比如P-256曲线的x坐标是31字节,直接编码后和32字节补位后的编码结果完全不同,会导致JWK解析失败。
2. 使用常规Base64而非Base64URL
常规Base64包含+、/和=填充字符,这些字符在URL和JSON中可能需要额外转义,不符合JWK的规范要求,会导致跨场景传递时公钥失效。
3. crv字段和坐标不匹配
比如使用P-384曲线的坐标,却将crv字段设置为P-256,解析方会按照错误的长度解码坐标,得到错误的结果或者解析报错。
4. 遗漏y坐标
部分椭圆曲线支持压缩公钥格式,只需要x坐标和奇偶标识,但标准的JWK椭圆曲线公钥要求同时提供x和y两个坐标,遗漏y坐标会导致JWK不合法。
5. 整数转字节的字节序错误
坐标整数转字节时必须使用大端序(big-endian),如果使用小端序转换,得到的字节数组完全错误,编码后的JWK也无法正常使用。
解析JWK时的注意事项
解析JWK时也需要反向注意编码时的这些问题:
- 先校验kty是否为EC,crv是否为支持的曲线
- 将x和y的Base64URL字符串解码为字节数组,检查长度是否符合曲线的要求
- 将字节数组转换为整数时同样使用大端序
下面是解析JWK的示例代码:
import base64
import json
def base64url_decode(data):
# Base64URL解码,补全填充字符
padding = 4 - len(data) % 4
if padding != 4:
data += '=' * padding
return base64.urlsafe_b64decode(data)
def jwk_to_ec_pubkey(jwk_str):
jwk = json.loads(jwk_str)
if jwk.get('kty') != 'EC':
raise ValueError("不是椭圆曲线JWK")
crv = jwk.get('crv')
crv_length_map = {
'P-256': 32,
'P-384': 48,
'P-521': 66
}
if crv not in crv_length_map:
raise ValueError(f"不支持的曲线: {crv}")
# 解码x和y
x_bytes = base64url_decode(jwk['x'])
y_bytes = base64url_decode(jwk['y'])
# 检查字节长度
if len(x_bytes) != crv_length_map[crv] or len(y_bytes) != crv_length_map[crv]:
raise ValueError("坐标字节长度不符合曲线要求")
# 转换为整数
x_int = int.from_bytes(x_bytes, byteorder='big')
y_int = int.from_bytes(y_bytes, byteorder='big')
return x_int, y_int, crv
# 解析之前的示例JWK
test_jwk = {
"kty": "EC",
"crv": "P-256",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use": "sig"
}
x, y, crv = jwk_to_ec_pubkey(json.dumps(test_jwk))
print(f"解析得到的曲线: {crv}")
print(f"x坐标整数: {x}")
print(f"y坐标整数: {y}")
JWK椭圆曲线公钥坐标编码JSON_Web_Key修改时间:2026-06-27 00:30:42