KJUR jws jsrsasign:无法在 JWT.io 上验证 ES256 令牌

KJUR jws jsrsasign: Cannot validate ES256 token on JWT.io

我们正在尝试使用 KJUR jws 库为 Apple Search Ads 制作 JWT 令牌。我们正在使用 Apple 的 API 文档:

https://developer.apple.com/documentation/apple_search_ads/implementing_oauth_for_the_apple_search_ads_api

我们正在生成私钥(prime256v1曲线):

openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem

接下来我们从私钥生成一个 public 密钥:

openssl ec -in private-key.pem -pubout -out public-key.pem

接下来我们设置 header 和负载:

var tNow = KJUR.jws.IntDate.get('now');
var tEnd = KJUR.jws.IntDate.get('now + 1day');
var teamId = 'SEARCHADS.xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
var keyId = 'xxxxxx-xxxx-xxxx-xxxxxxxxxxx';
var privateKey = `-----BEGIN EC PRIVATE KEY-----
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END EC PRIVATE KEY-----`;
  
var oHeader = {
  "alg": "ES256",
  "kid": keyId
}
  
var oPayload = {
  "iss": teamId,
  "iat": tNow,
  "exp": tEnd,
  "aud": "https://appleid.apple.com",
  "sub": clientId
}
   
var sHeader = JSON.stringify(oHeader);
var sPayload = JSON.stringify(oPayload);
  
var sKey = KEYUTIL.getKey({d: privateKey, curve: 'prime256v1'});  
var sResult = KJUR.jws.JWS.sign('ES256', sHeader, sPayload, sKey);

接下来我们尝试在 jwt.io 上验证 JWT 令牌(它已生成令牌)但无法验证。 Apple 搜索广告还会抛出 invalid_client 消息。我错过了什么?有人知道我在这里做错了什么吗?

亲切的问候,

杰克·夸克曼

问题是由于密钥导入不正确造成的。

发布的密钥是 SEC1 格式的 PEM 编码私钥。在 getKey() 中,密钥以 JWK 格式传递,指定原始私钥 d。 PEM 编码的 SEC1 密钥用作 d 的值。这是不正确的,因为原始私钥与 SEC1 密钥不同,而只是包含在其中。

要解决此问题,必须正确导入密钥。 jsrsasign也支持导入SEC1格式的PEM编码密钥,但是还需要EC参数,s。例如here。对于 prime256v1 又名 secp256r1 这是:

-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----

这些可以创建,例如使用 OpenSSL 作为密钥生成过程的一部分:

openssl ecparam -name secp256r1 -genkey

有了这个,固定的JavaScript代码是:

var tNow = KJUR.jws.IntDate.get('now');
var tEnd = KJUR.jws.IntDate.get('now + 1day');
var teamId = 'SEARCHADS.xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
var keyId = 'xxxxxx-xxxx-xxxx-xxxxxxxxxxx';
var privateKey = `-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIK1vV4iLOPym9KvJJU5hd6CMEp+DTt8QI7NPBdJSf+VDoAoGCCqGSM49
AwEHoUQDQgAEMpHT+HNKM7zjhx0jZDHyzQlkbLV0xk0H/TFo6gfT23ish58blPNh
YrFI51Q/czvkAwCtLZz/6s1n/M8aA9L1Vg==
-----END EC PRIVATE KEY-----`;
  
var oHeader = {
  "alg": "ES256",
  "kid": keyId
}
  
var oPayload = {
  "iss": teamId,
  "iat": tNow,
  "exp": tEnd,
  "aud": "https://appleid.apple.com",
  "sub": "clientId"
}
   
var sHeader = JSON.stringify(oHeader);
var sPayload = JSON.stringify(oPayload);
  
var sKey = KEYUTIL.getKey(privateKey);  
var sResult = KJUR.jws.JWS.sign('ES256', sHeader, sPayload, sKey);

document.getElementById("jwt").innerHTML = sResult;
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/10.4.0/jsrsasign-all-min.js"></script>
<p style="font-family:'Courier New', monospace;" id="jwt"></p>

可以使用以下 public 密钥(与上述私钥关联)在 https://jwt.io/ 上成功验证使用此代码生成的 JWT:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMpHT+HNKM7zjhx0jZDHyzQlkbLV0
xk0H/TFo6gfT23ish58blPNhYrFI51Q/czvkAwCtLZz/6s1n/M8aA9L1Vg==
-----END PUBLIC KEY-----

当然,正如评论中所说,私钥也可以转换为PKCS#8格式(例如使用OpenSSL)。同样可以使用 getKey()(或者 KEYUTIL.getKeyFromPlainPrivatePKCS8PEM())进行导入:

var tNow = KJUR.jws.IntDate.get('now');
var tEnd = KJUR.jws.IntDate.get('now + 1day');
var teamId = 'SEARCHADS.xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
var keyId = 'xxxxxx-xxxx-xxxx-xxxxxxxxxxx';
var privateKey = `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrW9XiIs4/Kb0q8kl
TmF3oIwSn4NO3xAjs08F0lJ/5UOhRANCAAQykdP4c0ozvOOHHSNkMfLNCWRstXTG
TQf9MWjqB9PbeKyHnxuU82FisUjnVD9zO+QDAK0tnP/qzWf8zxoD0vVW
-----END PRIVATE KEY-----`;
  
var oHeader = {
  "alg": "ES256",
  "kid": keyId
}
  
var oPayload = {
  "iss": teamId,
  "iat": tNow,
  "exp": tEnd,
  "aud": "https://appleid.apple.com",
  "sub": "clientId"
}
   
var sHeader = JSON.stringify(oHeader);
var sPayload = JSON.stringify(oPayload);
  
var sKey = KEYUTIL.getKey(privateKey);  
//var sKey = KEYUTIL.getKeyFromPlainPrivatePKCS8PEM(privateKey); // works also
var sResult = KJUR.jws.JWS.sign('ES256', sHeader, sPayload, sKey);

document.getElementById("jwt").innerHTML = sResult;
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/10.4.0/jsrsasign-all-min.js"></script>
<p style="font-family:'Courier New', monospace;" id="jwt"></p>

如果密钥作为 JWK 导入,除了原始私钥 d 之外,还必须指定原始 public 密钥的 xy 坐标.使用 ASN.1 解析器(例如 https://lapo.it/asn1js/)最容易确定这些值。此外,必须指定密钥类型 (kty),曲线标识符的关键字是 crv:

var tNow = KJUR.jws.IntDate.get('now');
var tEnd = KJUR.jws.IntDate.get('now + 1day');
var teamId = 'SEARCHADS.xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
var keyId = 'xxxxxx-xxxx-xxxx-xxxxxxxxxxx';
var privateKey = `rW9XiIs4_Kb0q8klTmF3oIwSn4NO3xAjs08F0lJ_5UM`;
var publicKeyX = `MpHT-HNKM7zjhx0jZDHyzQlkbLV0xk0H_TFo6gfT23g`;
var publicKeyY = `rIefG5TzYWKxSOdUP3M75AMArS2c_-rNZ_zPGgPS9VY`;
  
var oHeader = {
  "alg": "ES256",
  "kid": keyId
}
  
var oPayload = {
  "iss": teamId,
  "iat": tNow,
  "exp": tEnd,
  "aud": "https://appleid.apple.com",
  "sub": "clientId"
}
   
var sHeader = JSON.stringify(oHeader);
var sPayload = JSON.stringify(oPayload);
  
var sKey = KEYUTIL.getKey({kty: "EC", d: privateKey, x: publicKeyX, y: publicKeyY, crv: 'prime256v1'});  
var sResult = KJUR.jws.JWS.sign('ES256', sHeader, sPayload, sKey);

document.getElementById("jwt").innerHTML = sResult;
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/10.4.0/jsrsasign-all-min.js"></script>
<p style="font-family:'Courier New', monospace;" id="jwt"></p>

可以使用上面的 public 密钥在 https://jwt.io/ 上成功验证由这些代码生成的 JWT。