0

I have a public key in PEM format that was generated with:

ecdsa_public_key = OpenSSL::PKey::EC.new ecdsa_private_key
ecdsa_public_key.private_key = nil
ecdsa_public_key.to_pem

I have to read the PEM string and get a base64 url encoded string. How can I do that in Ruby?

ecdsa_public_key = OpenSSL::PKey.read pem_string
ecdsa_public_key.to_base64 # pseudo code...

BTW I have to do this for the WebPush protocol, which states:

you must add your VAPID public key to the Crypto-Key header as a base64 url encoded string with p256ecdsa= prepended to it.

collimarco
  • 34,231
  • 36
  • 108
  • 142

3 Answers3

4

The PEM string actually is base 64 encoded (at least partially), but I don’t think it’s what you want here, it includes other details and I think you want the “raw” public key data.

Here’s one way you can get your key into the format I think you want. It’s a bit long winded but I don’t think Ruby’s OpenSSL bindings provide a more direct method (you’ll need to require "base64" first):

# Assuming the key is in ecdsa_public_key
Base64.urlsafe_encode64(ecdsa_public_key.public_key.to_bn.to_s(2), padding: false)

This calls public_key to get the underlying OpenSSL::PKey::EC::Point, then converts that to an OpenSSL::BN in the correct format, and converts that to a binary string. Finally this string is base64 encoded.

matt
  • 78,533
  • 8
  • 163
  • 197
  • Thank you!! I'll try to use it in the next days and see if it works. I use Ruby 2.2.2: it doesn't support `padding: false`. Is it the same if I call `base64str.chomp('=').chomp('=')` in order to remove any `=` at the end? – collimarco Feb 06 '17 at 22:01
  • 1
    @collimarco Or you could use `str.delete("=")`, [which is what 2.4.0 does](https://github.com/ruby/ruby/blob/v2_4_0/lib/base64.rb#L85). I was basing my code on the example from https://developers.google.com/web/updates/2016/07/web-push-interop-wins which doesn’t include the `=`. It might work if you leave them, but it might be safer to strip them. – matt Feb 06 '17 at 23:04
  • I can't even try it because I have another issue (which may be strictly related): http://stackoverflow.com/questions/42079185/generate-the-vapid-public-key-in-rails-and-pass-it-to-javascript – collimarco Feb 06 '17 at 23:32
  • Thanks! I've accepted the answer because I've made a try and it works with Chrome! BTW I still have other issues with Firefox and the public key... – collimarco Feb 07 '17 at 12:18
0

Sorry that this is a bit late.

I'm not terribly familiar with Ruby, so I can't offer code examples for what to do, but I can try to describe the process of VAPID. (Also, my apologies if I go into needless detail, since I figure that others might stumble across this.)

In short, VAPID is a Javascript Web Token (JWT). You create a ECDSA key pair using your favorite ECDSA generation method specifically for VAPID.

e.g.

openssl ecparam -name prime256v1 -genkey -noout -out vapid_private.pem
openssl ec -in vapid_private.pem -pubout -out vapid_public.pem

A "PEM" is a formatted file that includes a standard header line, a footer line, and a set of long strings of crap. (Not sure if that's the technical term for them, but yeah, I'm going to go with that.) Those long strings of crap are Base64 representations of the key data saved in specific formats.

Honestly, I'd STRONGLY encourage using a library where and when possible. jwt.io has a number of Ruby libraries you could use, as well as libraries for other languages. As for the "Crypto-Key:" header, there's a bit of good news/other news.

  1. You could just take the Long Strings of Crap from your vapid-public.pem file, append them together and specify them as the 'p256ecdsa=' key.
  2. The VAPID protocol is changing soon https://datatracker.ietf.org/doc/html/draft-ietf-webpush-vapid-02

The change effectively gets rid of the Crypto-Key p256ecdsa component. Instead, the Authorization key becomes:

Authorization: vapid t=JWT containing VAPID info,k=VAPID Public key

e.g.

vapid t=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtYWlsdG86d2VicHVzaF9vcHNAY2F0ZmFjdHMuZXhhbXBsZS5jb20iLCJleHAiOjE0ODc0NDM3MTR9.mlLOWYMt-6aM3NB6b6_Msf8LqRKCuHd1Vfdp_fuJ3eqsQoID8lit305hIfNubTbvfACucuCygF3qB4scDbuHvg,k=EJwJZq_GN8jJbo1GGpyU70hmP2hbWAUpQFKDByKB81yldJ9GTklBM5xqEwuPM7VuQcyiLDhvovthPIXx-gsQRQ

I'm of mixed opinion about this. It does reduce the need for a separate header, but also shortens the amount of info you can shove into your claims before you run out of header room.

Community
  • 1
  • 1
JR Conlin
  • 11
  • 1
-1

You could try

  require 'base64'
  Base64.encode64(ecdsa_public_key)

to convert to base64

kitz
  • 879
  • 2
  • 9
  • 24
  • It returns `TypeError: no implicit conversion of OpenSSL::PKey::EC into String` – collimarco Feb 06 '17 at 15:44
  • If you call `.to_s` explicitly you get `#` – collimarco Feb 06 '17 at 15:46
  • Your object shold have to_text method [http://ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/PKey/EC.html#method-i-to_text]. So, you can do `Base64.encode64(ecdsa_public_key.to_text)` – kitz Feb 06 '17 at 16:24
  • `.to_text` returns other information, which is not strictly the key: `"Private-Key: (256 bit)\npub: \n ..."` – collimarco Feb 06 '17 at 19:44