[WebPush] [VAPID]请求失败,400 UnauthorizedRegistration

时间:2016-09-05 19:20:40

标签: java google-chrome web-push vapid

我正在使用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也产生了相同的结果。

一些代码片段用于澄清:

KeyGeneration:

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

ECPublicKey to 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草案03的进一步研究给出了以下部分的答案:2.3 链接:https://tools.ietf.org/html/draft-ietf-webpush-encryption-03 此外,Vincent Cheung的答案中的链接给出了一个很好的解释

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

  • 任何想法为什么FCM服务器会一直返回:400 UnauthorizedRegistration?

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

3 个答案:

答案 0 :(得分:5)

  

注册回复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是正确的做法吗?

将Bearer与JWT一起使用。

  

任何想法为什么FCM服务器会一直返回:400 UnauthorizedRegistration?

这是令人沮丧的部分:来自FCM的UnauthorizedRegistration并没有真正告诉你太多。对我来说,问题在于JWT标题的编组。我正在写我的Go,我正在编组一个包含&#34; typ&#34;和&#34; alg&#34;领域。我不认为JWT规范说明了字段的排序,但FCM显然想要一个特定的标题。当我看到使用constant header的实现时,我才意识到这一点。

我通过使用上面的标题编组替换我正在创建的标题解决了400问题。

还有一些其他小事需要注意:

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

  2. 您的JWT的Base64需要在没有填充的情况下进行URLE编码。显然还有另一个带有base64编码的Chrome错误,所以你需要处理它。这是一个考虑到这个bug的库中的一个例子。

  3. 编辑:我显然需要10个声誉才能发布2个以上的链接。查找&#34; push-encryption-go&#34;在Github和webpush / encrypt.go文件中,第118-130行处理chrome中的base64错误。

答案 1 :(得分:2)

对FCM的推送请求失败的主要问题是签名编码。我总是认为签名和哈希一样,只是一个未编码的字节流。但是,ECDSA签名包含R和S部分,在java中,这些部分在ASN.1 DER中表示,对于JWT,它们需要连接而无需进一步编码。

从技术上讲,这解决了我的问题。我还在努力完成这个库,并在完成后发布完整的解决方案(也许在GitHub上)。

答案 2 :(得分:2)

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