Skip to content

Commit

Permalink
Merge branch 'rsa-private-key-to-jwk-export' into rsa-jwk-improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
anakinj committed Oct 1, 2020
2 parents 47ec596 + 9b5d19f commit 3f10d36
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 24 deletions.
11 changes: 11 additions & 0 deletions README.md
Expand Up @@ -486,6 +486,17 @@ jwks = { keys: [{ ... }] } # keys needs to be Symbol
JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks})
```

### Importing and exporting JSON Web Keys

The ::JWT::JWK class can be used to import and export both the public key (default behaviour) and the private key. To include the private key in the export pass the `include_private` parameter to the export method.

```ruby
jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))

jwk_hash = jwk.export
jwk_hash_with_private_key = jwk.export(include_private: true)
```

# Development and Tests

We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
Expand Down
67 changes: 45 additions & 22 deletions lib/jwt/jwk/rsa.rb
Expand Up @@ -30,45 +30,68 @@ def kid
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
end

def export
{
def export(options = {})
ret = {
kty: KTY,
n: encode_open_ssl_bn(public_key.n),
e: encode_open_ssl_bn(public_key.e),
kid: kid
}

return ret if options[:include_private] != true

ret.merge(
d: encode_open_ssl_bn(keypair.d),
p: encode_open_ssl_bn(keypair.p),
q: encode_open_ssl_bn(keypair.q),
dp: encode_open_ssl_bn(keypair.dmp1),
dq: encode_open_ssl_bn(keypair.dmq1),
qi: encode_open_ssl_bn(keypair.iqmp)
)
end

def encode_open_ssl_bn(key_part)
::Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false)
end

def self.import(jwk_data)
jwk_n = jwk_data[:n] || jwk_data['n']
jwk_e = jwk_data[:e] || jwk_data['e']
class << self
def import(jwk_data)
self.new(rsa_pkey(*jwk_attrs(jwk_data, :n, :e, :d, :p, :q, :dp, :dq, :qi)), jwk_data[:kid])
end

raise JWT::JWKError, 'Key format is invalid for RSA' unless jwk_n && jwk_e
def jwk_attrs(jwk_data, *attrs)
attrs.map do |attr|
decode_open_ssl_bn(jwk_data[attr] || jwk_data[attr.to_s])
end
end

self.new(rsa_pkey(jwk_n, jwk_e), jwk_data[:kid])
end
def rsa_pkey(jwk_n, jwk_e, jwk_d, jwk_p, jwk_q, jwk_dp, jwk_dq, jwk_qi)
raise JWT::JWKError, 'Key format is invalid for RSA' unless jwk_n && jwk_e

def self.rsa_pkey(jwk_n, jwk_e)
key = OpenSSL::PKey::RSA.new
key_n = decode_open_ssl_bn(jwk_n)
key_e = decode_open_ssl_bn(jwk_e)
key = OpenSSL::PKey::RSA.new

if key.respond_to?(:set_key)
key.set_key(key_n, key_e, nil)
else
key.n = key_n
key.e = key_e
end
if key.respond_to?(:set_key)
key.set_key(jwk_n, jwk_e, jwk_d)
key.set_factors(jwk_p, jwk_q) if jwk_p && jwk_q
key.set_crt_params(jwk_dp, jwk_dq, jwk_qi) if jwk_dp && jwk_dq && jwk_qi
else
key.n = jwk_n
key.e = jwk_e
key.d = jwk_d if jwk_d
key.p = jwk_p if jwk_p
key.q = jwk_q if jwk_q
key.dmp1 = jwk_dp if jwk_dp
key.dmq1 = jwk_dq if jwk_dq
key.iqmp = jwk_qi if jwk_qi
end

key
end
key
end

def self.decode_open_ssl_bn(jwk_data)
OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data), BINARY)
def decode_open_ssl_bn(jwk_data)
return nil if jwk_data.nil?
OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data), BINARY)
end
end
end
end
Expand Down
22 changes: 20 additions & 2 deletions spec/jwk/rsa_spec.rb
Expand Up @@ -34,7 +34,7 @@
it 'returns a hash with the public parts of the key' do
expect(subject).to be_a Hash
expect(subject).to include(:kty, :n, :e, :kid)
expect(subject).not_to include(:d)
expect(subject).not_to include(:d, :p, :dp, :dq,:qi)
end
end

Expand All @@ -43,7 +43,7 @@
it 'returns a hash with the public parts of the key' do
expect(subject).to be_a Hash
expect(subject).to include(:kty, :n, :e, :kid)
expect(subject).not_to include(:d)
expect(subject).not_to include(:d, :p, :dp, :dq,:qi)
end
end

Expand All @@ -53,6 +53,15 @@
expect { subject }.to raise_error(ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA')
end
end

context 'when private key is requested' do
subject { described_class.new(keypair).export(include_private: true) }
let(:keypair) { rsa_key }
it 'returns a hash with the public AND private parts of the key' do
expect(subject).to be_a Hash
expect(subject).to include(:kty, :n, :e, :kid, :d, :p, :q, :dp, :dq,:qi)
end
end
end

describe '.import' do
Expand All @@ -77,6 +86,15 @@
end
end

context 'when private key is included in the data' do
let(:exported_key) { described_class.new(rsa_key).export(include_private: true) }
let(:params) { exported_key }
it 'creates a complete keypair' do
expect(subject).to be_a described_class
expect(subject.private?).to eq true
end
end

context 'when jwk_data is given without e and/or n' do
let(:params) { { kty: "RSA" } }
it 'raises an error' do
Expand Down

0 comments on commit 3f10d36

Please sign in to comment.