[WebPush][VAPID] Запрос не выполняется с 400 UnauthorizedRegistration
Я работаю над чистой реализацией Java для WebPush с VAPID и шифрованием полезной нагрузки (я уже сделал реализации для GCM и FCM). Однако документация по-прежнему незначительна, а также примеры кода по-прежнему не являются существенными. В данный момент я пытаюсь заставить его работать в Chrome. Несмотря на то, что я получаю успешные подписки, используя VAPID, когда я отправляю push-сообщение Tickle или Payload, я получаю 400 UnauthorizedRegistration. Я предполагаю, что это как-то связано с заголовком авторизации или заголовком Crypto-Key. Это то, что я отправляю пока для Tickle (push-уведомление без полезной нагрузки):
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)"
Я удалил пробелы из обоих JSON в JWT, так как спецификация не очень ясна в отношении использования пробелов, которое казалось наиболее безопасным. Подпись проверяется после декодирования 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 к 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);
Некоторые вопросы, которые находятся в моей голове:
для чего используется поле auth в ответе на регистрацию JSON? Поскольку, насколько мне известно, для шифрования только p256dh используется для генерации ключей шифрования вместе с KeyPair на основе сервера.
Дальнейшее исследование проекта IETF 03 дало мне ответ в разделе: 2.3 Ссылка: https://tools.ietf.org/html/draft-ietf-webpush-encryption-03 Также ссылка в ответе Винсента Ченга дает хорошее объяснение
Документация говорит о различном использовании заголовка для VAPID с использованием Bearer/WebPush и использованием заголовка Crypto-Key или заголовка Encryption-Key. Ват это правильно сделать?
- Любые идеи, почему сервер FCM продолжает возвращать: 400 UnauthorizedRegistration?
Может кто-нибудь добавить тег VAPID к этому вопросу? Кажется, он еще не существует.
3 ответа
Основная проблема в сбое push-запроса к FCM заключалась в кодировке подписи. Я всегда думал о сигнатуре так же, как о хеше, только о незакодированном потоке байтов. Однако подпись ECDSA содержит части R и S, в java они представлены в DER ASN.1, а для JWT их необходимо объединить без дальнейшего кодирования.
Технически это решает мой вопрос. Я все еще работаю над завершением библиотеки и опубликую полное решение здесь (и, возможно, на GitHub), когда оно будет закончено.
для чего используется поле auth в ответе на регистрацию JSON? Поскольку, насколько мне известно, для шифрования только p256dh используется для генерации ключей шифрования вместе с KeyPair на основе сервера.
Поле auth используется для шифрования, если вы отправляете push-уведомление, содержащее данные. Я не эксперт в крипто, но вот сообщение в блоге от Mozilla, которое объясняет это. https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/
Документация говорит о различном использовании заголовка для VAPID с использованием Bearer/WebPush и использованием заголовка Crypto-Key или заголовка Encryption-Key. Ват это правильно сделать?
Используйте Bearer с вашим JWT.
Любые идеи, почему сервер FCM продолжает возвращать: 400 UnauthorizedRegistration?
Это расстраивающая часть: UnauthorizedRegistration от FCM на самом деле не говорит вам много. Для меня проблема заключалась в сортировке заголовка JWT. Я писал свой в Go и собирал структуру, которая содержала поля "typ" и "alg". Я не думаю, что спецификация JWT что-то говорит о порядке полей, но FCM явно хотел определенный заголовок. Я понял это только тогда, когда увидел реализацию, которая использовала постоянный заголовок.
Я решил проблему 400, заменив заголовок, который я создал с помощью marshalling, на заголовок выше.
Есть и другие мелочи, на которые стоит обратить внимание:
В Chrome есть ошибка с заголовком Crypto-Key: если заголовок содержит более одной записи (т. Е. Шифрование полезной нагрузки также потребует использования заголовка crypto-key), вам нужно будет использовать точку с запятой вместо запятой, так как ваш разделитель
Base64 вашего JWT должен быть закодирован URLE без заполнения. Очевидно, есть еще одна ошибка в Chrome с кодировкой base64, поэтому вам нужно позаботиться об этом. Вот пример из библиотеки, которая учитывает эту ошибку.
Изменить: мне, видимо, мне нужно 10 репутации, чтобы разместить более 2 ссылок. Найдите "push-encryption-go" на Github и в файле webpush / encrypt.go, строки 118-130 устраняют ошибку base64 из chrome.
У меня такая же проблема. Решено путем удаления "gcm_sender_id" из манифеста JSON.