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://tools.ietf.org/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.