
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字节。
在实际开发中,尤其是在使用第三方密码学库(如 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 并不一致。
要正确地从椭圆曲线点对象中提取并编码 x 和 y 坐标,需要解决上述两个问题。
elliptic.js 库中的 Point 对象,其 toJSON() 方法可能不会直接返回用于JWK的规范化 x 和 y 坐标。正确的做法是使用 getX() 和 getY() 方法,它们返回的是 BN (BigNumber) 对象,代表了曲线点的规范化坐标值。
将上述代码中的坐标提取部分修改为:
// ...
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));
// ...JWK规范要求 x 和 y 坐标的大端字节表示必须填充到固定长度。这个长度取决于所使用的椭圆曲线。
因此,在将 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));
};结合上述两点修正,以下是正确从私钥派生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 完全一致。
通过理解JWK规范中椭圆曲线公钥坐标的编码要求,并正确处理坐标的规范化和字节长度填充,可以确保手动生成的JWK公钥与标准保持一致,从而实现不同系统间的无缝互操作。
以上就是JWK椭圆曲线公钥坐标编码详解与常见陷阱的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号