3

I'm working on a pure java implementation for WebPush with VAPID and payload encryption (I've already made implementations for GCM and FCM). However the documentation is still marginal and also the code samples are still not substantial. At this moment i'm trying to get it to work in Chrome. allthough i get succesful subscriptions using VAPID, when i send either a Tickle or a Payload push message i get a 400 UnauthorizedRegistration. My guess is that it has something to do with the authorization header or the Crypto-Key header. This is what i'm sending so far for a Tickle (A push notification without payload):

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)"

I've stripped the whitespaces from both JSON's in the JWT since the spec is not very clear about whitespace usage that seemed the safest thing to do. The signature validates after decoding the x9.62 to ECPoint again, so the publicKey seems validly encoded. However i keep getting the response:

<HTML><HEAD><TITLE>UnauthorizedRegistration</TITLE></HEAD><BODY BGCOLOR="#FFFFFF" TEXT="#000000"><H1>UnauthorizedRegistration</H1><H2>Error 400</H2></BODY></HTML>

According to the FCM documentation this only happends when a JSON error occurs, however i feel the specification does not cover WebPush at all. For now i've both tried the build in Java Crypto providers and BC both produce the same results.

Some code Snippets for clarification:

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;
}

Signing the JWT claim:

    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);

Some questions that reside in my mind:

  • 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.

    Further research of the ietf draft 03 gave me the answer in section: 2.3 Link: https://datatracker.ietf.org/doc/html/draft-ietf-webpush-encryption-03 Also the link in Vincent Cheung's answer gives a good explanation

  • 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?

  • Any ideas why the FCM server keeps returning a: 400 UnauthorizedRegistration ?

Can somebody add the VAPID tag to this question? It does not yet seem to exist.

Community
  • 1
  • 1
Bas Goossen
  • 459
  • 1
  • 7
  • 20
  • Mean-whilst i've been doing some tests using the https://web-push-libs.github.io/vapid/js/ An interesting find is that even though the signatory, signature and the ECPoint x and y values are equal to what i have server side for signing the JWT. Even so the vapid test page signature validation yields false. Could it be that the Java implementation (both sun and BC) of "SHA256withECDSA" is not compatible with the Javascript: {name: "ECDSA", namedCurve: "P-256", hash: {name: "SHA-256" }}; – Bas Goossen Sep 07 '16 at 14:33
  • Just got a little step further, now need to redo some of the former tests with order and so on. It seems that the encoding of the Java signature is in ASN.1 DER and the format for the JWT is Concatenated R+S. This explains the failure of validating the signature in my last comment. However even with that changed, i still get 400 UnauthorizedRegistration replies. – Bas Goossen Sep 07 '16 at 16:42
  • @bruha thanks for adding the vapid tag! – Bas Goossen Sep 28 '16 at 09:02
  • np, did you make Chrome work via web-push protocol? We got 201 response but push doesn't come. FF works perfectly. – bruha Sep 28 '16 at 11:55
  • Yes Crome works meanwhilst, both with and without payload without using GCM. If you get a 201 but the notification is not received it seems that there is something wrong with the encryption or the auth... Messages that cannot be decrypted will simply not be passed in Chrome. – Bas Goossen Sep 28 '16 at 11:56
  • Yep, but why it works in FF with the same data? Something weird with that Chrome. – bruha Sep 28 '16 at 12:14

3 Answers3

7

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.

The auth field is used for encryption if you're sending a push notification that contains data. I'm not an expert in crypto, but here's a blog post from Mozilla that explains it. 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?

Use Bearer with your JWT.

Any ideas why the FCM server keeps returning a: 400 UnauthorizedRegistration ?

This is the frustrating part: the UnauthorizedRegistration from FCM doesn't really tell you much. For me, the issue was with the marshalling of the JWT header. I was writing mine in Go and I was marshalling a struct that contained the "typ" and "alg" fields. I don't think the JWT spec says anything about the ordering of the fields, but FCM clearly wanted a specific header. I only realized this when I saw an implementation that used a constant header.

I resolved the 400 issue by replacing the header I was creating via marshalling with the header above.

There are some other small things you should look out for:

  1. Chrome has a bug with the Crypto-Key header: If the header has more than one entry (ie: encrypting a payload will also require the use of the crypto-key header), you will need to use a semicolon instead of a comma as your separator

  2. Base64 of your JWT needs to be URLEncoded without padding. There's apparently another Chrome bug with base64 encoding so you will need to take care of that. Here's an example from a library that takes this bug into consideration.

Edit: I apparently I need 10 reputations to post more than 2 links. Find "push-encryption-go" on Github and in the webpush/encrypt.go file, lines 118-130 takes care of the base64 bug from chrome.

  • Thank you for your great effort! This might give some clues, sadly though my JWT header is encoded equal to the static string in the link you provided. However since FCM is so picky on the JWT header i won't be surprised if it is on the JWT payload as well, i'll try some different orders there to be sure. In the registration object of Chrome there is indeed padding on the base64 encoded strings. However those are used server side only and it does not give any problems while decoding, so this is also not the core of my problem i assume. – Bas Goossen Sep 06 '16 at 19:25
  • Just tested all possible orders for the JWT Payload, without luck... Now wrestling my way through the Go source code (i've got no experience in Go, so it's a bit confusing for now). One thing that a like about your findings is that on a very small "error" in the JWT header it gave the same error, so hopefully that is the right direction to look at. – Bas Goossen Sep 06 '16 at 19:39
2

The main problem in the failed push request to FCM was in the Signature encoding. I allways thought of a signature the same as a hash, just an unencoded byte stream. However an ECDSA Signature contains an R and S portion, in java these are represented in ASN.1 DER and for JWT they need to be concatenated without further encoding.

Technically this solves my question. I'm still working on completing the library and will post the full solution here (and maybe on GitHub) when it is finished.

Bas Goossen
  • 459
  • 1
  • 7
  • 20
  • A question i have about this: What is the use of a signature beïng more than an unencoded stream of bytes? What can we do more with the R+S portions that validating the signature? Or is an ECDSA signature validated in another way than say an RSA signature? where in the validation process the R+S portions are required. – Bas Goossen Sep 09 '16 at 09:49
2

I had the same problem. Solved by removing the "gcm_sender_id" from the JSON manifest.

gvieira
  • 21
  • 5