[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, на заголовок выше.

Есть и другие мелочи, на которые стоит обратить внимание:

  1. В Chrome есть ошибка с заголовком Crypto-Key: если заголовок содержит более одной записи (т. Е. Шифрование полезной нагрузки также потребует использования заголовка crypto-key), вам нужно будет использовать точку с запятой вместо запятой, так как ваш разделитель

  2. Base64 вашего JWT должен быть закодирован URLE без заполнения. Очевидно, есть еще одна ошибка в Chrome с кодировкой base64, поэтому вам нужно позаботиться об этом. Вот пример из библиотеки, которая учитывает эту ошибку.

Изменить: мне, видимо, мне нужно 10 репутации, чтобы разместить более 2 ссылок. Найдите "push-encryption-go" на Github и в файле webpush / encrypt.go, строки 118-130 устраняют ошибку base64 из chrome.

У меня такая же проблема. Решено путем удаления "gcm_sender_id" из манифеста JSON.

Другие вопросы по тегам