0

0

JWK椭圆曲线公钥坐标编码详解与常见陷阱

碧海醫心

碧海醫心

发布时间:2025-09-24 09:00:03

|

1044人浏览过

|

来源于php中文网

原创

jwk椭圆曲线公钥坐标编码详解与常见陷阱

本文深入探讨了JSON Web Key (JWK) 中椭圆曲线公钥坐标的正确编码方法。针对从私钥派生公钥时常见的坐标未规范化和字节长度填充不足问题,提供了详细的解决方案和代码示例。通过遵循规范化的坐标提取和正确的字节填充策略,确保生成的JWK公钥与标准保持一致,实现互操作性。

1. JWK椭圆曲线公钥坐标编码规范

JSON Web Key (JWK) 是一种用于表示加密密钥的JSON数据结构。对于椭圆曲线 (EC) 公钥,其 x 和 y 坐标是关键组成部分。根据 JWK 规范,x 和 y 成员应包含椭圆曲线点的 x 和 y 坐标。它们必须以大端字节序 (big-endian) 表示,然后进行 Base64url 编码。

例如,对于P-521曲线,其坐标表示需要固定长度,即521位需要向上取整到下一个8的倍数,即528位,也就是66字节。这意味着无论实际数值大小,x 和 y 的大端字节表示都必须填充到66字节。

2. 从私钥派生公钥的常见问题

在实际开发中,尤其是在使用第三方密码学库(如 elliptic.js)从私钥派生公钥并尝试手动构造JWK时,开发者常常会遇到生成的 x 和 y 坐标与使用 crypto.subtle.exportKey 等标准API导出的结果不匹配的问题。这通常源于两个核心原因:坐标未规范化和字节长度填充不足。

让我们通过一个示例代码来演示这个问题:

const elliptic = require('elliptic');
const EC = elliptic.ec;
const {base16, base64url} = require('rfc4648');
const BN = require("bn.js");

// 辅助函数:将BN对象转换为Base64url字符串,但未进行固定长度填充
const padBase16ToWholeOctets = s => s.length % 2 === 0 ? s : '0' + s;
const bnToB64 = n => base64url.stringify(base16.parse(padBase16ToWholeOctets(n.toString(16))));

(async () => {
  console.log('--- 初始尝试 ---');
  let keyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-521" }, true, ['sign']);
  let jwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
  console.log('导出的JWK私钥部分:', jwk);

  const dHex = base16.stringify(base64url.parse(jwk.d, { loose: true }));

  const ec = new EC('p521');
  // 错误:toJSON() 可能不会提供规范化的坐标
  const [x_bn, y_bn] = ec.curve.g.mul(new BN(dHex, 16, 'be')).toJSON();

  console.log(`预期 x: ` + jwk.x);
  console.log(`实际 x (toJSON): ` + bnToB64(x_bn)); // 可能会不匹配
  console.log(`预期 y: ` + jwk.y);
  console.log(`实际 y (toJSON): ` + bnToB64(y_bn)); // 可能会不匹配
  console.log('----------------');
})();

运行上述代码,你会发现 实际 x 和 实际 y 的输出与 预期 x 和 预期 y 并不一致。

3. 解决方案:规范化坐标与字节填充

要正确地从椭圆曲线点对象中提取并编码 x 和 y 坐标,需要解决上述两个问题。

3.1 坐标规范化

elliptic.js 库中的 Point 对象,其 toJSON() 方法可能不会直接返回用于JWK的规范化 x 和 y 坐标。正确的做法是使用 getX() 和 getY() 方法,它们返回的是 BN (BigNumber) 对象,代表了曲线点的规范化坐标值。

将上述代码中的坐标提取部分修改为:

Anyword
Anyword

AI文案写作助手和文本生成器,具有可预测结果的文案 AI

下载
// ...
const ec = new EC('p521');
const point = ec.curve.g.mul(new BN(dHex, 16, 'be')); // 计算公钥点

// 正确:使用 getX() 和 getY() 获取规范化坐标
const x_bn_normalized = point.getX();
const y_bn_normalized = point.getY();

console.log(`实际 x (getX): ` + bnToB64(x_bn_normalized));
console.log(`实际 y (getY): ` + bnToB64(y_bn_normalized));
// ...

3.2 字节长度填充

JWK规范要求 x 和 y 坐标的大端字节表示必须填充到固定长度。这个长度取决于所使用的椭圆曲线。

  • P-256 曲线: 256位 / 8 = 32字节
  • P-384 曲线: 384位 / 8 = 48字节
  • P-521 曲线: 521位,向上取整到下一个8的倍数是528位 / 8 = 66字节

因此,在将 BN 对象转换为十六进制字符串后,需要将其填充到对应的字节长度。对于P-521曲线,66字节对应132个十六进制字符。

修改 bnToB64 辅助函数,加入固定长度的零填充:

// 辅助函数:将BN对象转换为Base64url字符串,并进行固定长度填充
const bnToB64Padded = (n, byteLength) => {
  const hexString = padBase16ToWholeOctets(n.toString(16));
  // 填充到指定字节长度的十六进制字符串(每个字节2个十六进制字符)
  const paddedHexString = hexString.padStart(byteLength * 2, '0');
  return base64url.stringify(base16.parse(paddedHexString));
};

4. 完整示例与正确实践

结合上述两点修正,以下是正确从私钥派生P-521曲线公钥并生成JWK x 和 y 坐标的完整代码:

const elliptic = require('elliptic');
const EC = elliptic.ec;
const {base16, base64url} = require('rfc4648');
const BN = require("bn.js");

// 辅助函数:将BN对象转换为Base64url字符串,并进行固定长度填充
const padBase16ToWholeOctets = s => s.length % 2 === 0 ? s : '0' + s;
const bnToB64Padded = (n, byteLength) => {
  const hexString = padBase16ToWholeOctets(n.toString(16));
  // 填充到指定字节长度的十六进制字符串(每个字节2个十六进制字符)
  const paddedHexString = hexString.padStart(byteLength * 2, '0');
  return base64url.stringify(base16.parse(paddedHexString));
};

(async () => {
  console.log('--- 正确实践 ---');
  // 1. 生成P-521 ECDSA密钥对
  let keyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-521" }, true, ['sign']);
  // 2. 导出JWK格式的私钥,包含公钥的x, y坐标
  let jwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
  console.log('导出的完整JWK:', jwk);

  // 3. 从JWK私钥中提取私钥参数 'd'
  const dHex = base16.stringify(base64url.parse(jwk.d, { loose: true }));

  // 4. 初始化elliptic曲线对象 (P-521)
  const ec = new EC('p521');
  // 5. 使用私钥参数 'd' 和基点 'g' 计算公钥点
  const point = ec.curve.g.mul(new BN(dHex, 16, 'be'));

  // 6. 提取规范化的 x 和 y 坐标 (BN对象)
  const x_bn_normalized = point.getX();
  const y_bn_normalized = point.getY();

  // 7. 定义P-521曲线的坐标字节长度 (521位 -> 66字节)
  const P521_BYTE_LENGTH = 66;

  // 8. 将规范化坐标转换为Base64url编码字符串,并进行固定长度填充
  const actual_x_b64 = bnToB64Padded(x_bn_normalized, P521_BYTE_LENGTH);
  const actual_y_b64 = bnToB64Padded(y_bn_normalized, P521_BYTE_LENGTH);

  console.log(`预期 x (来自crypto.subtle): ` + jwk.x);
  console.log(`实际 x (手动计算):        ` + actual_x_b64);

  console.log(`预期 y (来自crypto.subtle): ` + jwk.y);
  console.log(`实际 y (手动计算):        ` + actual_y_b64);

  // 验证是否匹配
  console.log(`x 匹配结果: ${jwk.x === actual_x_b64}`);
  console.log(`y 匹配结果: ${jwk.y === actual_y_b64}`);
  console.log('----------------');
})();

运行这段代码,你会发现 实际 x 和 实际 y 的输出与 预期 x 和 预期 y 完全一致。

5. 注意事项与总结

  • 曲线类型与字节长度: 务必根据所使用的椭圆曲线类型(如P-256, P-384, P-521)确定正确的字节长度进行填充。这是确保JWK互操作性的关键。
  • 库函数选择: 在使用第三方密码学库时,要仔细查阅其API文档,了解如何获取规范化的坐标值。避免使用可能返回非规范化或非固定长度表示的方法(如某些库的 toJSON())。
  • Base64url编码: 确保使用正确的Base64url编码,而不是标准的Base64编码。Base64url不包含 +, /, = 字符,而是使用 -, _,且不带填充。
  • 安全性: 在生产环境中,推荐优先使用浏览器内置的 WebCrypto API (crypto.subtle) 或经过严格审计的密码学库来处理密钥生成和导出,以确保安全性和合规性。

通过理解JWK规范中椭圆曲线公钥坐标的编码要求,并正确处理坐标的规范化和字节长度填充,可以确保手动生成的JWK公钥与标准保持一致,从而实现不同系统间的无缝互操作。

相关专题

更多
json数据格式
json数据格式

JSON是一种轻量级的数据交换格式。本专题为大家带来json数据格式相关文章,帮助大家解决问题。

415

2023.08.07

json是什么
json是什么

JSON是一种轻量级的数据交换格式,具有简洁、易读、跨平台和语言的特点,JSON数据是通过键值对的方式进行组织,其中键是字符串,值可以是字符串、数值、布尔值、数组、对象或者null,在Web开发、数据交换和配置文件等方面得到广泛应用。本专题为大家提供json相关的文章、下载、课程内容,供大家免费下载体验。

533

2023.08.23

jquery怎么操作json
jquery怎么操作json

操作的方法有:1、“$.parseJSON(jsonString)”2、“$.getJSON(url, data, success)”;3、“$.each(obj, callback)”;4、“$.ajax()”。更多jquery怎么操作json的详细内容,可以访问本专题下面的文章。

310

2023.10.13

go语言处理json数据方法
go语言处理json数据方法

本专题整合了go语言中处理json数据方法,阅读专题下面的文章了解更多详细内容。

75

2025.09.10

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

258

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

209

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1468

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

620

2023.11.24

Java编译相关教程合集
Java编译相关教程合集

本专题整合了Java编译相关教程,阅读专题下面的文章了解更多详细内容。

5

2026.01.21

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
WEB前端教程【HTML5+CSS3+JS】
WEB前端教程【HTML5+CSS3+JS】

共101课时 | 8.4万人学习

JS进阶与BootStrap学习
JS进阶与BootStrap学习

共39课时 | 3.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号