Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebPush over UnifiedPush #15

Closed
karmanyaahm opened this issue Sep 8, 2021 · 28 comments
Closed

WebPush over UnifiedPush #15

karmanyaahm opened this issue Sep 8, 2021 · 28 comments
Labels
connector About connector libraries hard Lots of work required server Server side (push provider, gateway, etc)

Comments

@karmanyaahm
Copy link
Member

A compatibility layer. Needs implementations in connectors and a rewrite proxy, afaict. It would be very useful to be able to rely on all the existing crypto for an e2ee UnifiedPush experience.

@karmanyaahm karmanyaahm added hard Lots of work required connector About connector libraries server Server side (push provider, gateway, etc) labels Sep 8, 2021
@p1gp1g
Copy link
Member

p1gp1g commented Oct 4, 2021

It looks like we don't need a rewrite-proxy (https://www.rfc-editor.org/rfc/rfc8291.html#section-5)

Also, we wrote the registration for android for fedilab a few months ago, it would not require a long time to use it for a library

@strider72
Copy link

strider72 commented Oct 5, 2021

What is an example use case of this? What problem does it fix? Does web push on Android normally go through Google FCM?

@strider72
Copy link

I guess my answer is this (from Element chat):

(Without unifiedpush) : There are 2 ways to use webpush :

  • with a lib and every app using it have a connection wayting for incoming requests
  • with a lib that does queries to a single app that listen for incoming requests for all the other apps

Unifiedpush only specify how internal app sends data and how server send to distributor. Actually, it is possible to have a webpush distributor

So UP is that single app that listens for requests

@p1gp1g
Copy link
Member

p1gp1g commented Oct 5, 2021

IIRC, browsers use FCM for webpush. Also it is usefull for app implementing webpush (e.g. mastodon)

@karmanyaahm
Copy link
Member Author

What is an example use case of this? What problem does it fix? Does web push on Android normally go through Google FCM?

This advantage is not necessarily for web browsers, but rather being able to use UP with application servers that already support webpush, and also benefitting from the encryption at the same time.

@emersion
Copy link

There's no need for a rewrite proxy.

@p1gp1g
Copy link
Member

p1gp1g commented Dec 1, 2021

Indeed. For webpush, salt, content-size and public key are sended following RFC8188 in the post data. source: RFC8291

   +-----------+--------+-----------+---------------+
   | salt (16) | rs (4) | idlen (1) | keyid (idlen) |
   +-----------+--------+-----------+---------------+

Example:


   POST /push/JzLQ3raZJfFBR0aqvOMsLrt54w4rJUsV HTTP/1.1
   Host: push.example.net
   TTL: 10
   Content-Length: 145
   Content-Encoding: aes128gcm

   DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27ml
   mlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPT
   pK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN

Salt: DGv6ra1nlYgDCS1FRnbzlw
Application server public key: BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8


Without a rewrite proxy, we only miss :

@karmanyaahm
Copy link
Member Author

So I checked the following libraries, and they accept all 2XX status codes, don't check response headers, and don't support any advanced features (delivery receipts) of webpush based on my quick survey:

  • Ruby: The most popular ruby library on rubygems.org called webpush and Mastodon's own implementation
  • Go: SherClockHolmes/webpush, the top GitHub result for a Go webpush library
  • Python: popular library (on Pypi) jazzband/django-push-notifications which is based on web-push-libs/pywebpush
  • JS: web-push on NPM

The only limitation with pywebpush is >202 response code is marked as failing.

So from my rough survey, not adhering to rfc8030 super tightly (with response headers and extra but mandatory features such as receipt notifications) should be fine for most applications. i.e. another need for a reverse proxy - push response - is alleviated

@karmanyaahm
Copy link
Member Author

karmanyaahm commented Apr 4, 2022

So, in terms of library API, we have two main options (if there are more good options, you can comment them below). This broadly applies to all libraries Android, Flutter, Linux

Integrated

The old API:

func Register(instance string, distributor string)
func onNewEndpoint(endpoint string)
func Unregister(instance string)

the new API

func Register(instance string, distributor string)
func RegisterWithWebpush(instance string, distributor string)
func onNewEndpoint(endpoint string, p256dh string, auth string) // if using webpush, p256dh = auth = ""  and can be ignored
func Unregister(instance string) 

Here, onMessage will automatically provide the decrypted payload if registration is done with WebPush

Separate library

In an onNewEndpoint call, devs will call

func (webpush) generateKeys(instance string) (p256dh string, auth string) //this instance doesn't have to be the same as the UP library, but probably will be for simplicity 
// it should save the private key automatically

Then in onMessage, devs will call

func (webpush) decrypt(instance string, ciphertext []byte)  (plaintext []byte)
//uses the stored keys for that instance and decrypts the text

I think integrated will be simpler for devs (fewer imports, automatic decryption), but might result in more code to maintain for the core UP libraries - vice-versa for a separate library

Also, if we want to encourage WebPush/encryption adoption for all UP apps, I think integrated is the way to go (since then there's one less step to take for encryption by app devs)

Comments are welcome from everyone :), which do you prefer?

@p1gp1g
Copy link
Member

p1gp1g commented Apr 4, 2022

The old API should be :

func Register(instance string, distributor string)
func onNewEndpoint(instance string, endpoint string)
func Unregister(instance string)

On my side, I see a 3rd option : a "unifiedpush_webpush" library that depends on the unifiedpush library (connector) :

func Register(instance string, distributor string)
func onNewEndpoint(instance string, endpoint string, p256dh string, auth string) // if using webpush, p256dh = auth = ""  and can be ignored
func Unregister(instance string)

karmanyaahm added a commit to karmanyaahm/specifications that referenced this issue Apr 8, 2022
Changing 202 to 201 is a very insignificant change since 2xx is required to be accepted (and most webpush implementations accept 2xx anyway UnifiedPush/wishlist#15 (comment))

However, it is a minor way in which UnifiedPush can be similar to WebPush and could matter to pickier implementations. 201 is required in webpush. https://datatracker.ietf.org/doc/html/rfc8030#section-5
p1gp1g pushed a commit to UnifiedPush/specifications that referenced this issue Apr 13, 2022
Changing 202 to 201 is a very insignificant change since 2xx is required to be accepted (and most webpush implementations accept 2xx anyway UnifiedPush/wishlist#15 (comment))

However, it is a minor way in which UnifiedPush can be similar to WebPush and could matter to pickier implementations. 201 is required in webpush. https://datatracker.ietf.org/doc/html/rfc8030#section-5
@karmanyaahm
Copy link
Member Author

karmanyaahm commented Jan 9, 2023

Just doing some reading.

This was the change in (the future) RFC8188 that resulted in moving keys from headers to the body: httpwg/http-extensions#252

This is where it started affecting WebPush: https://mailarchive.ietf.org/arch/msg/webpush/aLfZBx6wZRKMZ7X2AG_t1X2TlIE/ webpush-wg/webpush-encryption#9

https://mailarchive.ietf.org/arch/msg/webpush/19pz0qIZBNN0GheAmKu72cYLr9k/

it may be layered over Webcoket, newly proposed WiSH, etc - and in many
cases the implementation will be greatly simplified if the binary blob can
be sent as-is (after any protocol-specific authentication ).

totally agree with Costin

@karmanyaahm
Copy link
Member Author

karmanyaahm commented Jan 9, 2023

ok so, gateway idea:

1. Discovery

Turn {"unifiedpush":{"version":1}} into {"unifiedpush":{"version":1,"cryptokey":1}} if this gateway is supported.

2. Gateway operation

If Content-Encoding == "aesgcm", convert the data in "crypto-key" and "encryption" to the following body header used in aes128gcm WebPush.

   +-----------+--------+-----------+---------------+
   | salt (16) | rs (4) | idlen (1) | keyid (idlen) |
   +-----------+--------+-----------+---------------+

OR Alternative Gateway operation

If "crypto-key" and "encryption" headers exist, simply append them to the message body. This trades server complexity for client library complexity, which then has to parse and decrypt both formats.


If the "cryptokey" gateway is not supported by the endpoint, fall back to https://cryptokey.gateway.unifiedpush.org/?url=https://oldversion.ntfy.sh/1234 or something. If every push server natively supports this, the backup gateway should be rarely used, avoiding centralization risks. In a few months (to allow for everyone to update), Tusky and Fedilab can avoid wake+poll.

Like Matrix, this is probably simple and non-intrusive enough to be included natively in push servers. I will experiment with this stuff as soon as I have some time.

@karmanyaahm
Copy link
Member Author

karmanyaahm commented Jan 9, 2023

also, a ton of application servers still use the old protocol (aesgcm) (image from March 2021, but I doubt it's changed that much)

image
mozilla/rust-ece#53 (comment)

@karmanyaahm
Copy link
Member Author

  1. Gateway operation

So, aesgcm and aes128gcm use different nonces and context strings and stuff, so it is not possible to transparently convert between the two formats on the server...

I spent way too long figuring this out ☹️.

So to appending headers it is.

@karmanyaahm
Copy link
Member Author

karmanyaahm commented Jan 12, 2023

@p1gp1g
Copy link
Member

p1gp1g commented Feb 16, 2023

UP-Example now supports WebPush: https://github.com/UnifiedPush/android-example/tree/1.5.2

I don't think it is useful to have yet another lib for WebPush over UnifiedPush.

What do you think about leaving flutter-connector-webpush too ? We probably can just write an example.

@karmanyaahm
Copy link
Member Author

Yeah, it's probably fine if we make a docs page explaining how to use the Store and WebPush classes. Maybe the actual decryption could also be a function in the library, just to create a strong boundary between the 'scary cryptography' parts and app logic?

@karmanyaahm
Copy link
Member Author

image
mozilla/rust-ece#53 (comment)

Hi @jrconlin 👋

UnifiedPush is an open source protocol for Android (and Linux) push notifications. As we're working on increased WebPush compatibility, I was curious as to what that graph looks like nowadays? Is aesgcm still dominant?

Thanks for all your open source WebPush tooling.

@jrconlin
Copy link

Is aesgcm still dominant?

Sigh. If anything, it's gotten worse.
image

Apparently, if you provide libraries for an early version of a protocol, expect that version to live forever.

Just to recap, aesgcm is based on a draft of the webpush encryption RFC, where the Diffie Hellman public key is URL safe, unpadded, base64 encoded into the Crypto-Key: dh= header parameter, and the Salt is encoded into the Encryption: salt= header parameter. For that content type, the recipient decryption system expects those values to be included as part of the delivered message.

For aes128gcm encoding, those values are prefixed to the encrypted body. Granted, there are additional complications if you're dealing with a multi-part push message, but those are fairly rare because they're very complicated to implement. (Basically, at some point, it becomes TCP over WebPush.)

Theoretically, you might be able to convert between aesgcm and aes128gcm, since the values produced are the same, but I've never tried doing it, and I long since learned not to invoke the wrath of the cryptography gods.

@karmanyaahm
Copy link
Member Author

karmanyaahm commented May 23, 2023

Apparently, if you provide libraries for an early version of a protocol, expect that version to live forever.

🙁. Yeah, part of the issue we've noticed is on the app side too. For example, Mastodon has various WebPush -> FCM/APNS proxies that use aesgcm and so is unable to exclusively switch to aes128gcm. Worse, the ruby webpush library v1.0 only supports aes128gcm (not both), so it would be an ambitious endeavor to even get it to support both.

I kinda hoped Apple, having the market power, would only support aes128gcm when they started supporting WebPush, but that did not happen.

For that content type, the recipient decryption system expects those values to be included as part of the delivered message.
For aes128gcm encoding, those values are prefixed to the encrypted body.

Yes, handling headers is the main complication in adding aesgcm to UP. But I guess if it's that popular we have to do it anyway.

Theoretically, you might be able to convert between aesgcm and aes128gcm, since the values produced are the same, but I've never tried doing it, and I long since learned not to invoke the wrath of the cryptography gods.

I tried doing that, but realized that the nonces for key derivation are different on the two standards. So, unfortunately it won't be possible to adapt on the push server (without private keys).

Thanks for the information though!

@jrconlin
Copy link

Ah, crap, you're right. I forgot about the nonces.

The silly thing is that I reached out to a bunch of the library authors a couple of years ago and encouraged them to switch to aes128gcm, for a LOT of reasons. A number of them did, indeed, make the default aes128gcm. Sadly, the folk actually implementing things, apparently, either never update their libraries or actively set the encryption to the older standard. It's a point of confusion and frustration for me.

(Extra fun fact: supporting both on the message server is also a serious pain. Technically, there were three standards for a while but we dropped the oldest. I would love to be able to work with the other folk at Google, Apple and Microsoft to declare aesgcm unsupported as of some future date. I'm going to guess that they'd probably be on board with that idea since it would greatly simplify things on their end, but the problem is that it's surprisingly hard to get in touch with those teams.)

@p1gp1g
Copy link
Member

p1gp1g commented May 24, 2023

To summarize:

  • Not supporting the webpush draft, may reduce the number of applications that could use UnifiedPush (except with a sync on push strategy).
  • Supporting the draft adds complexity to the specification, and pushes things in the wrong direction (inertia to support the specification).

IMO, it is better to stay with the webpush specification, sync on push is sufficient for applications that only support the draft specification. What do you think ?


Concerning Mastodon, I think the best thing is to publish a draft-webpush gem, based on webpush gem so they can support both

@jrconlin
Copy link

FWIW, I'm absolutely fine with only working against the RFC spec. Feel free to note that the RFC has been out for 7 years and that most major WebPush libraries support it. Make using aesgcm a pain point for folk. (Granted, email with "+" in the local part has been a thing even longer than that and still there are tons of sites that think [email protected] is invalid, but that's a different problem.)

Needless to say, I'll be cheering for y'all so that folk finally stop using the old specification.

@MzHub
Copy link

MzHub commented Dec 11, 2023

IMO, it is better to stay with the webpush specification, sync on push is sufficient for applications that only support the draft specification. What do you think?

Considering that currently there is no WebPush support, I strongly agree with progress towards at least standards compatible WebPush support.

The draft version support will hopefully become less relevant over time, and if it doesn't it can be reconsidered to be solved later?

The standard support hopefully becomes more relevant over time.

@p1gp1g
Copy link
Member

p1gp1g commented Dec 11, 2023

UnifiedPush is compatible with WebPush. Its support is limited (e.g. urgency is ignored) but since an accepted push message returns a 201 with TTL: 0, application servers shouldn't try to use WebPush's unimplemented features (for instance, the app server won't try to delete a pending message).

@p1gp1g
Copy link
Member

p1gp1g commented Jan 12, 2024

What do you think about closing this issue ? UP supports (in a limited way, with TTL: 0) WebPush. This opened issue seems to add some confusion

If someone feels the need to, maybe we can open an issue to Extend WebPush support to add TTL>0, the ability to delete messages, even urgency etc.

@p1gp1g
Copy link
Member

p1gp1g commented Jan 17, 2024

I'm closing it then.

If someone feels the need to, maybe we can open an issue to Extend WebPush support to add TTL>0, the ability to delete messages, even urgency etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
connector About connector libraries hard Lots of work required server Server side (push provider, gateway, etc)
Projects
None yet
Development

No branches or pull requests

6 participants