【问题标题】:[WebPush][VAPID] Request fails with 400 UnauthorizedRegistration[WebPush][VAPID] 请求失败,出现 400 UnauthorizedRegistration
【发布时间】:2017-01-13 04:11:15
【问题描述】:

我正在为带有 VAPID 和有效负载加密的 WebPush 开发一个纯 Java 实现(我已经为 GCM 和 FCM 实现了)。然而,文档仍然是边际的,而且代码示例仍然不是实质性的。目前我正试图让它在 Chrome 中工作。尽管我使用 VAPID 获得了成功的订阅,但当我发送 Tickle 或 Payload 推送消息时,我会收到 400 UnauthorizedRegistration。我的猜测是它与授权标头或 Crypto-Key 标头有关。到目前为止,这是我为 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 Crypto 提供程序中构建,并且 BC 都产生了相同的结果。

一些代码片段用于澄清:

密钥生成:

KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "BC");
ECGenParameterSpec spec = new ECGenParameterSpec("secp256r1");
keyGen.initialize(spec, secureRandom);
KeyPair vapidPair = keyGen.generateKeyPair();

ECPublicKey 到 x9.62:

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 中的 auth 字段是什么?因为据我所知,只有 p256dh 与基于服务器的 KeyPair 一起用于生成加密密钥。

    对ietf draft 03的进一步研究在第2.3节给了我答案 链接:https://datatracker.ietf.org/doc/html/draft-ietf-webpush-encryption-03 Vincent Cheung的答案中的链接也给出了很好的解释

  • 文档谈到了使用 Bearer/WebPush 和使用 Crypto-Key 标头或 Encryption-Key 标头的 VAPID 的不同标头用法。 Wat 是正确的做法吗?

  • 任何想法为什么 FCM 服务器不断返回: 400 UnauthorizedRegistration ?

有人可以在这个问题中添加 VAPID 标签吗?它似乎还不存在。

【问题讨论】:

  • 意思是,虽然我一直在使用web-push-libs.github.io/vapid/js 进行一些测试,但一个有趣的发现是,即使签名者、签名和 ECPoint x 和 y 值等于我在服务器端的值签署 JWT。即便如此,乏味的测试页面签名验证也会产生错误。难道是“SHA256withECDSA”的Java实现(sun和BC)与Javascript不兼容:{name:“ECDSA”,namedCurve:“P-256”,hash:{name:“SHA-256”} };
  • 刚刚进了一点点,现在需要重做一些以前的测试,以此类推。 Java签名的编码似乎是ASN.1 DER,JWT的格式是Concatenated R+S。这解释了我上一条评论中验证签名的失败。然而,即使改变了,我仍然收到 400 条 UnauthorizedRegistration 回复。
  • @bruha 感谢您添加 vapid 标签!
  • np,您是否让 Chrome 通过 web-push 协议工作?我们收到了 201 响应,但没有推送。 FF 完美运行。
  • 是的,Crome 在不使用 GCM 的情况下,无论有无负载,都可以正常工作。如果您收到 201 但未收到通知,则似乎加密或身份验证有问题...无法解密的消息将根本不会在 Chrome 中传递。

标签: java google-chrome web-push vapid


【解决方案1】:

注册回复 JSON 中的 auth 字段是什么?因为据我所知,只有 p256dh 与基于服务器的 KeyPair 一起用于生成加密密钥。

如果您发送包含数据的推送通知,则 auth 字段用于加密。我不是加密专家,但这里有一篇来自 Mozilla 的博客文章对此进行了解释。 https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/

文档谈到了使用 Bearer/WebPush 和使用 Crypto-Key 标头或 Encryption-Key 标头的 VAPID 的不同标头用法。 Wat 是正确的做法吗?

在您的 JWT 中使用 Bearer。

任何想法为什么 FCM 服务器不断返回: 400 UnauthorizedRegistration ?

这是令人沮丧的部分:来自 FCM 的 UnauthorizedRegistration 并不能真正告诉您太多信息。对我来说,问题在于 JWT 标头的编组。我正在用 Go 编写我的,并且正在编组一个包含“typ”和“alg”字段的结构。我认为 JWT 规范没有说明字段的顺序,但 FCM 显然想要一个特定的标题。当我看到一个使用 constant header 的实现时,我才意识到这一点。

我通过将我通过编组创建的标头替换为上面的标头解决了 400 问题。

还有一些其他的小事你应该注意:

  1. Chrome 的 Crypto-Key 标头存在一个错误:如果标头有多个条目(即:加密有效负载也需要使用加密密钥标头),您将需要使用用分号代替逗号作为分隔符

  2. 您的 JWT 的 Base64 需要在没有填充的情况下进行 URLEncoded。显然还有另一个带有 base64 编码的 Chrome 错误,因此您需要注意这一点。这是一个考虑到这个错误的库的例子。

编辑:我显然需要 10 个声誉才能发布超过 2 个链接。在 Github 和 webpush/encrypt.go 文件中找到“push-encryption-go”,第 118-130 行负责处理来自 chrome 的 base64 错误。

【讨论】:

  • 感谢您的努力!这可能会提供一些线索,但遗憾的是,尽管我的 JWT 标头的编码等于您提供的链接中的静态字符串。然而,由于 FCM 对 JWT 标头如此挑剔,如果它也在 JWT 有效负载上,我不会感到惊讶,我会尝试一些不同的命令来确定。在 Chrome 的注册对象中,base64 编码的字符串上确实有填充。但是,这些仅用于服务器端,在解码时不会出现任何问题,所以这也不是我假设的问题的核心。
  • 刚刚测试了 JWT Payload 的所有可能订单,没有运气......现在我正在努力研究 Go 源代码(我没有 Go 经验,所以现在有点混乱) .与您的发现类似的一件事是,在 JWT 标头中的一个非常小的“错误”上,它给出了相同的错误,所以希望这是正确的研究方向。
【解决方案2】:

对 FCM 的推送请求失败的主要问题在于签名编码。我一直认为签名与哈希相同,只是未编码的字节流。然而,ECDSA 签名包含 R 和 S 部分,在 java 中,它们以 ASN.1 DER 表示,对于 JWT,它们需要连接起来而不需要进一步编码。

从技术上讲,这解决了我的问题。我仍在努力完成库,完成后将在此处(可能在 GitHub 上)发布完整的解决方案。

【讨论】:

  • 我对此有一个问题:签名比未编码的字节流有什么用?我们可以用验证签名的 R+S 部分做更多的事情吗?或者 ECDSA 签名是否以另一种方式验证,而不是 RSA 签名?在验证过程中,需要 R+S 部分。
【解决方案3】:

我遇到了同样的问题。通过从 JSON 清单中删除“gcm_sender_id”解决。

【讨论】:

    猜你喜欢
    • 2020-12-28
    • 1970-01-01
    • 2023-01-25
    • 2017-01-16
    • 2014-08-02
    • 2017-06-02
    • 2020-10-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多