[WebPush][VAPID] 请求失败,出现 400 未经授权的注册
[WebPush][VAPID] Request fails with 400 UnauthorizedRegistration
我正在为具有 VAPID 和负载加密的 WebPush 开发纯 java 实现(我已经为 GCM 和 FCM 实现了)。然而,文档仍然很少,代码示例也仍然不多。目前我正试图让它在 Chrome 中工作。虽然我使用 VAPID 获得了成功的订阅,但当我发送 Tickle 或 Payload 推送消息时,我收到了 400 UnauthorizedRegistration。我的猜测是它与授权header或Crypto-Keyheader有关。这是我目前发送的 Tickle(没有负载的推送通知):
URL: https://fcm.googleapis.com/fcm/send/xxxxx:xxxxxxxxxxx...
Action: POST/PUT (Both give same result)
With headers:
Authorization: Bearer URLBase64(JWT_HEAD).URLBase64(JWT_Payload).SIGN
Crypto-Key: p265ecdsa=X9.62(PublicKey)
Content-Type: "text/plain;charset=utf8"
Content-Length: 0
TTL: 120
JWT_HEAD="{\"typ\":\"JWT\",\"alg\":\"ES256\"}"
JWT_Payload={
aud: "https://fcm.googleapis.com",
exp: (System.currentTimeMillis() / 1000) + (60 * 60 * 12)),
sub: "mailto:webpush@mydomain.com"
}
SIGN = the "SHA256withECDSA" signature algorithm over: "URLBase64(JWT_HEAD).URLBase64(JWT_Payload)"
我已经从 JWT 中的两个 JSON 中删除了空格,因为规范对空格的使用不是很清楚,这似乎是最安全的做法。
签名在 x9.62 再次解码为 ECPoint 后验证,因此 publicKey 似乎有效编码。但是我不断收到回复:
<HTML><HEAD><TITLE>UnauthorizedRegistration</TITLE></HEAD><BODY BGCOLOR="#FFFFFF" TEXT="#000000"><H1>UnauthorizedRegistration</H1><H2>Error 400</H2></BODY></HTML>
根据 FCM 文档,这仅在发生 JSON 错误时发生,但我认为该规范根本不涵盖 WebPush。现在我都尝试了内置 Java 加密提供商和 BC 都产生了相同的结果。
澄清一些代码片段:
密钥生成:
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "BC");
ECGenParameterSpec spec = new ECGenParameterSpec("secp256r1");
keyGen.initialize(spec, secureRandom);
KeyPair vapidPair = keyGen.generateKeyPair();
x9.62 的 ECPublicKey:
public byte[] toUncompressedPoint(ECPublicKey publicKey){
final ECPoint publicPoint = publicKey.getW();
final int keySizeBytes = (publicKey.getParams().getOrder().bitLength() + Byte.SIZE - 1) / Byte.SIZE;
final byte[] x = publicPoint.getAffineX().toByteArray();
final byte[] y = publicPoint.getAffineY().toByteArray();
final byte[] res = new byte[1 + 2 * keySizeBytes];
int offset = 0;
res[offset++] = 0x04; //Indicating no key compression is used
if(x.length <= keySizeBytes)
System.arraycopy(x, 0, res, offset + keySizeBytes - x.length, x.length);
else if(x.length == keySizeBytes + 1) System.arraycopy(x, 1, res, offset, keySizeBytes);
else throw new IllegalArgumentException("X value is too large!");
offset += keySizeBytes;
if(y.length <= keySizeBytes)
System.arraycopy(y, 0, res, offset + keySizeBytes - y.length, y.length);
else if(y.length == keySizeBytes + 1 && y[0] == 0) System.arraycopy(y, 1, res, offset, keySizeBytes);
else throw new IllegalArgumentException("Y value is too large!");
return res;
}
签署 JWT 声明:
ObjectNode claim = om.createObjectNode();
claim.put("aud", host);
claim.put("exp", (System.currentTimeMillis() / 1000) + (60 * 60 * 12));
claim.put("sub", "mailto:webpush_ops@mydomain.com");
String claimString = claim.toString();
String encHeader = URLBase64.encodeString(VAPID_HEADER, false);
String encPayload = URLBase64.encodeString(claimString, false);
String vapid = null;
ECPublicKey pubKey = (ECPublicKey) vapidPair.getPublic();
byte[] point = toUncompressedPoint(pubKey);
String vapidKey = URLBase64.encodeToString(point, false);
try{
Signature dsa = Signature.getInstance("SHA256withECDSA", "BC");
dsa.initSign(vapidPair.getPrivate());
dsa.update((encHeader + "." + encPayload).getBytes(StandardCharsets.US_ASCII));
byte[] signature = dsa.sign();
vapid = encHeader + "." + encPayload + "." + URLBase64.encodeToString(signature, false);
我心中的一些问题:
注册回复中的授权字段是什么JSON?因为据我所知,只有 p256dh 用于加密与基于服务器的 KeyPair 一起生成加密密钥。
对 ietf draft 03 的进一步研究在第 2.3 节中给出了答案
Link: https://datatracker.ietf.org/doc/html/draft-ietf-webpush-encryption-03
Vincent Cheung 的回答中的link也给出了很好的解释
文档谈到了 VAPID 的不同 header 用法,使用 Bearer/WebPush 和使用 Crypto-Key header 或 Encryption-Key header。正确的做法是什么?
知道为什么 FCM 服务器不断返回:400 UnauthorizedRegistration 吗?
有人可以为这个问题添加 VAPID 标签吗?它似乎还不存在。
what is the auth field for in the registration reply JSON? Since to my knowledge for encryption only the p256dh is used for generating the encryption keys together with a server based KeyPair.
如果您要发送包含数据的推送通知,则 auth 字段用于加密。我不是加密方面的专家,但 Mozilla 的博客 post 对其进行了解释。 https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/
The documentation speaks of different header usage for VAPID using Bearer/WebPush and using the Crypto-Key header or the Encryption-Key header. Wat is the correct thing to do?
将 Bearer 与 JWT 结合使用。
Any ideas why the FCM server keeps returning a: 400 UnauthorizedRegistration ?
这是令人沮丧的部分:FCM 的 UnauthorizedRegistration 并没有真正告诉您太多信息。对我来说,问题在于 JWT header 的编组。我正在用 Go 编写我的代码,并且正在编组一个包含 "typ" 和 "alg" 字段的结构。我不认为 JWT 规范对字段的顺序有任何说明,但 FCM 显然想要一个特定的 header。当我看到一个使用 constant header.
的实现时,我才意识到这一点
我通过用上面的 header 替换我通过编组创建的 header 解决了 400 问题。
您还应该注意一些其他小事项:
Chrome 与 Crypto-Key header 有一个错误:如果 header 有多个条目(即:加密负载将还需要使用 crypto-key header),您需要使用分号而不是逗号作为分隔符
您的 JWT 的 Base64 需要进行无填充的 URLEncoded。显然还有另一个 base64 编码的 Chrome 错误,所以你需要处理它。这是一个考虑了这个错误的库的例子。
编辑:我显然需要 10 个信誉才能 post 超过 2 个链接。在 Github 和 webpush/encrypt.go 文件中找到 "push-encryption-go",第 118-130 行解决了 chrome.
中的 base64 错误
对 FCM 的推送请求失败的主要问题在于签名编码。我一直认为签名和哈希一样,只是一个未编码的字节流。然而,ECDSA 签名包含 R 和 S 部分,在 java 中,这些在 ASN.1 DER 中表示,对于 JWT,它们需要连接起来而不需要进一步编码。
从技术上讲,这解决了我的问题。我仍在努力完成库,并会在完成后 post 完整的解决方案(可能在 GitHub 上)。
我遇到了同样的问题。通过从 JSON 清单中删除 "gcm_sender_id" 解决。
我正在为具有 VAPID 和负载加密的 WebPush 开发纯 java 实现(我已经为 GCM 和 FCM 实现了)。然而,文档仍然很少,代码示例也仍然不多。目前我正试图让它在 Chrome 中工作。虽然我使用 VAPID 获得了成功的订阅,但当我发送 Tickle 或 Payload 推送消息时,我收到了 400 UnauthorizedRegistration。我的猜测是它与授权header或Crypto-Keyheader有关。这是我目前发送的 Tickle(没有负载的推送通知):
URL: https://fcm.googleapis.com/fcm/send/xxxxx:xxxxxxxxxxx...
Action: POST/PUT (Both give same result)
With headers:
Authorization: Bearer URLBase64(JWT_HEAD).URLBase64(JWT_Payload).SIGN
Crypto-Key: p265ecdsa=X9.62(PublicKey)
Content-Type: "text/plain;charset=utf8"
Content-Length: 0
TTL: 120
JWT_HEAD="{\"typ\":\"JWT\",\"alg\":\"ES256\"}"
JWT_Payload={
aud: "https://fcm.googleapis.com",
exp: (System.currentTimeMillis() / 1000) + (60 * 60 * 12)),
sub: "mailto:webpush@mydomain.com"
}
SIGN = the "SHA256withECDSA" signature algorithm over: "URLBase64(JWT_HEAD).URLBase64(JWT_Payload)"
我已经从 JWT 中的两个 JSON 中删除了空格,因为规范对空格的使用不是很清楚,这似乎是最安全的做法。 签名在 x9.62 再次解码为 ECPoint 后验证,因此 publicKey 似乎有效编码。但是我不断收到回复:
<HTML><HEAD><TITLE>UnauthorizedRegistration</TITLE></HEAD><BODY BGCOLOR="#FFFFFF" TEXT="#000000"><H1>UnauthorizedRegistration</H1><H2>Error 400</H2></BODY></HTML>
根据 FCM 文档,这仅在发生 JSON 错误时发生,但我认为该规范根本不涵盖 WebPush。现在我都尝试了内置 Java 加密提供商和 BC 都产生了相同的结果。
澄清一些代码片段:
密钥生成:
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "BC");
ECGenParameterSpec spec = new ECGenParameterSpec("secp256r1");
keyGen.initialize(spec, secureRandom);
KeyPair vapidPair = keyGen.generateKeyPair();
x9.62 的 ECPublicKey:
public byte[] toUncompressedPoint(ECPublicKey publicKey){
final ECPoint publicPoint = publicKey.getW();
final int keySizeBytes = (publicKey.getParams().getOrder().bitLength() + Byte.SIZE - 1) / Byte.SIZE;
final byte[] x = publicPoint.getAffineX().toByteArray();
final byte[] y = publicPoint.getAffineY().toByteArray();
final byte[] res = new byte[1 + 2 * keySizeBytes];
int offset = 0;
res[offset++] = 0x04; //Indicating no key compression is used
if(x.length <= keySizeBytes)
System.arraycopy(x, 0, res, offset + keySizeBytes - x.length, x.length);
else if(x.length == keySizeBytes + 1) System.arraycopy(x, 1, res, offset, keySizeBytes);
else throw new IllegalArgumentException("X value is too large!");
offset += keySizeBytes;
if(y.length <= keySizeBytes)
System.arraycopy(y, 0, res, offset + keySizeBytes - y.length, y.length);
else if(y.length == keySizeBytes + 1 && y[0] == 0) System.arraycopy(y, 1, res, offset, keySizeBytes);
else throw new IllegalArgumentException("Y value is too large!");
return res;
}
签署 JWT 声明:
ObjectNode claim = om.createObjectNode();
claim.put("aud", host);
claim.put("exp", (System.currentTimeMillis() / 1000) + (60 * 60 * 12));
claim.put("sub", "mailto:webpush_ops@mydomain.com");
String claimString = claim.toString();
String encHeader = URLBase64.encodeString(VAPID_HEADER, false);
String encPayload = URLBase64.encodeString(claimString, false);
String vapid = null;
ECPublicKey pubKey = (ECPublicKey) vapidPair.getPublic();
byte[] point = toUncompressedPoint(pubKey);
String vapidKey = URLBase64.encodeToString(point, false);
try{
Signature dsa = Signature.getInstance("SHA256withECDSA", "BC");
dsa.initSign(vapidPair.getPrivate());
dsa.update((encHeader + "." + encPayload).getBytes(StandardCharsets.US_ASCII));
byte[] signature = dsa.sign();
vapid = encHeader + "." + encPayload + "." + URLBase64.encodeToString(signature, false);
我心中的一些问题:
注册回复中的授权字段是什么JSON?因为据我所知,只有 p256dh 用于加密与基于服务器的 KeyPair 一起生成加密密钥。
对 ietf draft 03 的进一步研究在第 2.3 节中给出了答案 Link: https://datatracker.ietf.org/doc/html/draft-ietf-webpush-encryption-03 Vincent Cheung 的回答中的link也给出了很好的解释
文档谈到了 VAPID 的不同 header 用法,使用 Bearer/WebPush 和使用 Crypto-Key header 或 Encryption-Key header。正确的做法是什么?
知道为什么 FCM 服务器不断返回:400 UnauthorizedRegistration 吗?
有人可以为这个问题添加 VAPID 标签吗?它似乎还不存在。
what is the auth field for in the registration reply JSON? Since to my knowledge for encryption only the p256dh is used for generating the encryption keys together with a server based KeyPair.
如果您要发送包含数据的推送通知,则 auth 字段用于加密。我不是加密方面的专家,但 Mozilla 的博客 post 对其进行了解释。 https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/
The documentation speaks of different header usage for VAPID using Bearer/WebPush and using the Crypto-Key header or the Encryption-Key header. Wat is the correct thing to do?
将 Bearer 与 JWT 结合使用。
Any ideas why the FCM server keeps returning a: 400 UnauthorizedRegistration ?
这是令人沮丧的部分:FCM 的 UnauthorizedRegistration 并没有真正告诉您太多信息。对我来说,问题在于 JWT header 的编组。我正在用 Go 编写我的代码,并且正在编组一个包含 "typ" 和 "alg" 字段的结构。我不认为 JWT 规范对字段的顺序有任何说明,但 FCM 显然想要一个特定的 header。当我看到一个使用 constant header.
的实现时,我才意识到这一点我通过用上面的 header 替换我通过编组创建的 header 解决了 400 问题。
您还应该注意一些其他小事项:
Chrome 与 Crypto-Key header 有一个错误:如果 header 有多个条目(即:加密负载将还需要使用 crypto-key header),您需要使用分号而不是逗号作为分隔符
您的 JWT 的 Base64 需要进行无填充的 URLEncoded。显然还有另一个 base64 编码的 Chrome 错误,所以你需要处理它。这是一个考虑了这个错误的库的例子。
编辑:我显然需要 10 个信誉才能 post 超过 2 个链接。在 Github 和 webpush/encrypt.go 文件中找到 "push-encryption-go",第 118-130 行解决了 chrome.
中的 base64 错误对 FCM 的推送请求失败的主要问题在于签名编码。我一直认为签名和哈希一样,只是一个未编码的字节流。然而,ECDSA 签名包含 R 和 S 部分,在 java 中,这些在 ASN.1 DER 中表示,对于 JWT,它们需要连接起来而不需要进一步编码。
从技术上讲,这解决了我的问题。我仍在努力完成库,并会在完成后 post 完整的解决方案(可能在 GitHub 上)。
我遇到了同样的问题。通过从 JSON 清单中删除 "gcm_sender_id" 解决。