web-push-libs/vapid

Given pair of public and private keys, Vapid throws invalid padding exception

mselevan opened this issue · 6 comments

The site I am using is https://web-push-codelab.appspot.com/

it seems to be the only one that will generate a valid private/key pair that works for Chrome, but, I cannot figure out a way to get the private key into Vapid(). I feel like I am missing an important step here but there is no documentation as to what type of string the Vapid() constructor is expecting. Would it be possible to add more detail?

ERROR:root:Could not open private key file: Error('Incorrect padding',)
Traceback (most recent call last):
  File "/Users/mselevan/dev/envs/next/lib/python3.5/site-packages/py_vapid/__init__.py", line 57, in __init__
    base64.urlsafe_b64decode(private_key))
  File "/Users/mselevan/dev/envs/next/lib/python3.5/base64.py", line 135, in urlsafe_b64decode
    return b64decode(s)
  File "/Users/mselevan/dev/envs/next/lib/python3.5/base64.py", line 90, in b64decode
    return binascii.a2b_base64(s)
binascii.Error: Incorrect padding

Now when I add some padding to that base64encoded private key, i get a different exception

ERROR:root:Could not open private key file: UnexpectedDER('wanted sequence (0x30), got 0x92',)
Traceback (most recent call last):
  File "/Users/mselevan/dev/envs/next/lib/python3.5/site-packages/py_vapid/__init__.py", line 57, in __init__
    base64.urlsafe_b64decode(private_key))
  File "/Users/mselevan/dev/envs/next/lib/python3.5/site-packages/ecdsa/keys.py", line 165, in from_der
    s, empty = der.remove_sequence(string)
  File "/Users/mselevan/dev/envs/next/lib/python3.5/site-packages/ecdsa/der.py", line 65, in remove_sequence
    raise UnexpectedDER("wanted sequence (0x30), got 0x%02x" % n)
ecdsa.der.UnexpectedDER: wanted sequence (0x30), got 0x92

ECDSA keys normally are "padded" to 4 octet boundaries with "=", which is a horribly complicated way of saying that you either need to stick a few "=" at the end of your private key, or more properly, I probably should just add that logic into pyVapid.

That said, normally my library reads the keys as PEM files (or DER strings). Both of these are kinda/sorta the same, where PEM adds a few header lines and makes all the lines max 65 characters (or octets). DER formatted keys also have a bit of formatting internally, and ECDSA private keys usually consist of three components

If you wanted, you can see these if you run something like:
openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem

(Yes, this is probably way more information than you need/want/desire, but I find that it helps others and myself)

It looks like the web-push-codelab page is providing a partial key for the private. (Normally a "private" key contains all values, including the x and y ones in the public key). It may be providing just the raw d value encoded into base64. That's kinda non-standard, and while the private key only really matters to you when you sign stuff, I'm willing to bet that having just the raw values isn't going to make a lot of libraries very happy.

That probably explains what you're seeing.

Now, as for what might be going on with chrome, there's a couple of things at play.

  1. Chrome only wants to use "restricted" URLs (which makes the "Voluntary" bit of VAPID a fair bit less so.
  2. You make a restricted URL by passing in the raw PUBLIC key (which is the x and y coordinates munged together with a prefix of "\04", and encoded into base64) and passed into the push endpoint registration.
  3. You then MUST sign inbound requests using VAPID and the corresponding private key before sending them on their merry way.

Aside from the fact that any URL you sign with a given key is only valid for that key (in other words, if you generate or change the key you use for VAPID stuff, any URL you previously signed up for is instantly invalid), it shouldn't matter how that key is generated.

My library currently wants PEM or DER format, because that's the standard way that keys are handled. I'm not really sure I want to deal with pure, raw values like what the codelab offers because I'd have to do guesswork, and frankly guesswork and cryptography never mix.

I'm currently sitting on a significant patch that introduces VAPID Draft 02 support. I'll add the padding fix for the key, and maybe a truncation check. I REALLY wish that chrome allowed for key import so that folks could run stand-alone test pages like https://mozilla-services.github.io/WebPushDataTestPage/, but that's also unlikely to happen. I will see about smoothing out pyvapid and chrome, now that chrome is a bit more stable about things.

Sorry for the long reply. Hope it helps.

@jrconlin thank you for the long response. Really appreciate the info! I am not sure I understand #2 and #3 in your potential solution. What exactly do you mean by passing the raw public key into the end point registration? Do you mean on the client side when requesting the subscription object? And if so, do you mean updating the PUBLIC key used to subscribe include that \04 and then base64 encode it?

We're working on a few solutions that should make this a lot easier.

When you call PushManager.subscribe(), you pass an applicationServerKey. This is ideally opaque to you, but in reality, it's the "raw" form of the public key. (Again, way too much information: but this value is two, very long integer values, encoded into byte arrays, glommed together and prefixed with a \04, which is then base64 encoded into a string.) For what it's worth, this is what the p256ecdsa component of the CryptoKey header is. You can also get this out of the openssl generated public key PEM file if you had used that to generate the key. It's also what codelab provides as it's public key.

I may also add a function to dump the applicationServerKey value as one of the things that the vapid tool can return.

@jrconlin thanks for the info. do you have any rough eta on when the update will be pushed? if not no worries. really appreciate the help

@jrconlin also, shot in the dark here, but would you have a suggestion for how i can convert the codelab private key into something that your library can use? at this point i believe I am correctly signing the endpoint url when fetching the subscription object, and the pair of keys i am using were generated via openssl like you had suggested. still no luck with chrome. encryption is not my forte so i am having a tough time finding any info on how i can convert the codelabs private key into a more complete private key

Well, if you want to play around with the version that's live in this repo, that should do it. The API changed a bit, thus the version number change.

You'll want to call Vapid.from_raw(codelab_private_key) to set the private key.