Search code examples
ruby-on-railsrubyopenssl

Creating RSA key with OpenSSL v3 using "modulus" and "exponent" doesn't work in Ruby on Rails


I have an RSA public key modulus and exponent string. I want to create a OpenSSL::PKey::RSA using them in Ruby on Rails. I tried it three different ways and I think something is messed up about my environement, however, I can't spot it. Do you have any idea?

Versions:

ruby 3.2.1 (2023-02-08 revision 31819e82c8) [arm64-darwin22]
Rails 7.0.8
OpenSSL 3.1.2 1 Aug 2023

First Try

I do this:

modulus = k['n']
exponent= k['e']
rsa = OpenSSL::PKey::RSA.new
rsa.set_key_rsa(k['n'], k['e'], 2)

and I get error:

undefined method `set_key_rsa' for #<OpenSSL::PKey::RSA:0x000000010a1468a0 oid=rsaEncryption>

Second try

I do this:

modulus = k['n']
exponent= k['e']
rsa = OpenSSL::PKey::RSA.new
rsa.set_key(k['n'], k['e'], 2) # <---- used set_key

I get error:

rsa#set_key= is incompatible with OpenSSL 3.0

Third

rsa = OpenSSL::PKey::RSA.new
rsa.n = OpenSSL::BN.new(Base64.urlsafe_decode64(k['n']), 2)
rsa.e = OpenSSL::BN.new(Base64.urlsafe_decode64(k['e']), 2)

I get error:

undefined method `n=' for #<OpenSSL::PKey::RSA:0x000000010ab56e08 oid=rsaEncryption>

Ugh, what's happening?


Solution

  • Jump to solution part if you are not interested in what I was trying.

    Context:

    I am creating an LTI tool that has a URL to get a keyset for verifying a JWT token sent from an LTI platform. Keys in the keyset has a modulus (n) and an exponent (e) value to create the RSA key that will decode the JWT.

    There were two problems,

    • in OpenSSL v3 n and e methods are not working anymore.
    • it is not clear how to format modulus (n) and exponent (e) values properly to use the in certain methods

    Solution:

    I found the first Gist that worked before OpenSSL v3, however it did not work in the OpenSSL v3. Thankfully, this great dude shared how to achieve this in OpenSSL v3 in the comments. Check out the second link for the Gist or the code below for the solution.

    Sharing the Gist code here provided in the second link (just in case):

    # Given n and e in typical encoding, like that found on a jwks well-known.
    # For example for google, from https://www.googleapis.com/oauth2/v3/certs
    n = "t0VFy4n4MGtbMWJKk5qfCY2WGBja2WSWQ2zsLziSx9p1QE0QgXtr1x85PnQYaYrAvOBiXm2mrxWnZ42MxaUUu9xyykTDxsNWHK--ufchdaqJwfqd5Ecu-tHvFkMIs2g39pmG8QfXJHKMqczKrvcHHJrpTqZuos1uhYM9gxOLVP8wTAUPNqa1caiLbsszUC7yaMO3LY1WLQST79Z8u5xttKXShXFv1CCNs8-7vQ1IB5DWQSR2um1KV4t42d31Un4-8cNiURx9HmJNJzOXbTG-vDeD6sapFf5OGDsCLO4YvzzkzTsYBIQy_p88qNX0a6AeU13enxhbasSc-ApPqlxBdQ"
    e = "AQAB"
    
    rsa = create_rsa_key(n, e)
    
    def create_rsa_key(n, e)
      data_sequence = OpenSSL::ASN1::Sequence([
                                                OpenSSL::ASN1::Integer(base64_to_long(n)),
                                                OpenSSL::ASN1::Integer(base64_to_long(e))
                                              ])
      asn1 = OpenSSL::ASN1::Sequence(data_sequence)
      OpenSSL::PKey::RSA.new(asn1.to_der)
    end
    
    def base64_to_long(data)
      decoded_with_padding = Base64.urlsafe_decode64(data) + Base64.decode64("==")
      decoded_with_padding.to_s.unpack("C*").map do |byte|
        byte_to_hex(byte)
      end.join.to_i(16)
    end
    
    def byte_to_hex(int)
      int < 16 ? "0" + int.to_s(16) : int.to_s(16)
    end
    

    Hope this helped!