From 5059ccad4927a1002b3a574616bb88e52891581b Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Fri, 7 Oct 2022 16:53:19 +0200 Subject: [PATCH 01/15] Added a `common_parameters` parameter to JWKs representing values from the IANA JSON Web Key Parameters Registry which are not specific to any key type --- lib/jwt/jwk/ec.rb | 18 +++++++++--------- lib/jwt/jwk/hmac.rb | 15 ++++++++------- lib/jwt/jwk/key_base.rb | 9 ++++++--- lib/jwt/jwk/rsa.rb | 10 +++++++--- spec/jwk/ec_spec.rb | 9 +++++++++ spec/jwk/hmac_spec.rb | 9 +++++++++ spec/jwk/rsa_spec.rb | 20 +++++++++++++++++++- spec/jwk_spec.rb | 23 +++++++++++++++++++---- 8 files changed, 86 insertions(+), 27 deletions(-) diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index 77cf7c44..83a32de1 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -37,7 +37,8 @@ def members end def export(options = {}) - exported_hash = members.merge(kid: kid) + kid # Make sure a kid is generated + exported_hash = common_parameters.merge(members) return exported_hash unless private? && options[:include_private] == true @@ -93,11 +94,16 @@ class << self def import(jwk_data) # See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an # explanation of the relevant parameters. + parameters = jwk_data.transform_keys(&:to_sym) + parameters.delete(:kty) # Will be re-added upon export + jwk_crv = parameters.delete(:crv) + jwk_x = parameters.delete(:x) + jwk_y = parameters.delete(:y) + jwk_d = parameters.delete(:d) - jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid]) raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y - new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), kid: jwk_kid) + new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), common_parameters: parameters) end def to_openssl_curve(crv) @@ -115,12 +121,6 @@ def to_openssl_curve(crv) private - def jwk_attrs(jwk_data, attrs) - attrs.map do |attr| - jwk_data[attr] || jwk_data[attr.to_s] - end - end - if ::JWT.openssl_3? def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength curve = to_openssl_curve(jwk_crv) diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index 98573a90..e7e2c8f0 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -25,10 +25,10 @@ def public_key # See https://tools.ietf.org/html/rfc7517#appendix-A.3 def export(options = {}) - exported_hash = { - kty: KTY, - kid: kid - } + exported_hash = common_parameters.merge({ + kty: KTY, + kid: kid + }) return exported_hash unless private? && options[:include_private] == true @@ -54,12 +54,13 @@ def key_digest class << self def import(jwk_data) - jwk_k = jwk_data[:k] || jwk_data['k'] - jwk_kid = jwk_data[:kid] || jwk_data['kid'] + parameters = jwk_data.transform_keys(&:to_sym) + parameters.delete(:kty) # Will be re-added upon export + jwk_k = parameters.delete(:k) raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k - new(jwk_k, kid: jwk_kid) + new(jwk_k, common_parameters: parameters) end end end diff --git a/lib/jwt/jwk/key_base.rb b/lib/jwt/jwk/key_base.rb index 8c796990..1385bf6a 100644 --- a/lib/jwt/jwk/key_base.rb +++ b/lib/jwt/jwk/key_base.rb @@ -9,20 +9,23 @@ def self.inherited(klass) end def initialize(options) - options ||= {} + options = options&.clone || {} # Beware: Only a shallow copy if options.is_a?(String) # For backwards compatibility when kid was a String options = { kid: options } end - @kid = options[:kid] + @common_parameters = options[:common_parameters]&.transform_keys(&:to_sym) || {} + @common_parameters[:kid] = options[:kid] if options[:kid] # kid can be specified outside common_parameters @kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator end def kid - @kid ||= generate_kid + @common_parameters[:kid] ||= generate_kid end + attr_accessor :common_parameters + private attr_reader :kid_generator diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index 5f6d2288..b4d0fcf5 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -14,7 +14,7 @@ def initialize(keypair, options = {}) raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA) @keypair = keypair - + super(options) end @@ -27,7 +27,8 @@ def public_key end def export(options = {}) - exported_hash = members.merge(kid: kid) + kid # Make sure a kid is generated + exported_hash = common_parameters.merge(members) return exported_hash unless private? && options[:include_private] == true @@ -70,7 +71,10 @@ def import(jwk_data) pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value| decode_open_ssl_bn(value) end - new(rsa_pkey(pkey_params), kid: jwk_attributes(jwk_data, :kid)[:kid]) + parameters = jwk_data.transform_keys(&:to_sym) + parameters.delete(:kty) # Will be re-added upon export + RSA_KEY_ELEMENTS.each { |e| parameters.delete e } + new(rsa_pkey(pkey_params), common_parameters: parameters) end private diff --git a/spec/jwk/ec_spec.rb b/spec/jwk/ec_spec.rb index f5ecc360..0397aaf9 100644 --- a/spec/jwk/ec_spec.rb +++ b/spec/jwk/ec_spec.rb @@ -69,6 +69,15 @@ expect(subject).to include(:d) end end + + context 'when a common parameter is given' do + let(:parameters) { { use: 'sig' } } + let(:keypair) { ec_key } + subject { described_class.new(keypair, common_parameters: parameters).export } + it 'returns a hash including the common parameter' do + expect(subject).to include(:use) + end + end end describe '.import' do diff --git a/spec/jwk/hmac_spec.rb b/spec/jwk/hmac_spec.rb index d7594077..b04ae709 100644 --- a/spec/jwk/hmac_spec.rb +++ b/spec/jwk/hmac_spec.rb @@ -58,6 +58,15 @@ expect(subject.kid).to eq('custom_key_identifier') end end + + context 'with a common parameter' do + let(:exported_key) { + super().merge(use: 'sig') + } + it 'imports that common parameter' do + expect(subject.common_parameters).to include(:use) + end + end end end end diff --git a/spec/jwk/rsa_spec.rb b/spec/jwk/rsa_spec.rb index c9228778..d04bc1e4 100644 --- a/spec/jwk/rsa_spec.rb +++ b/spec/jwk/rsa_spec.rb @@ -78,11 +78,29 @@ end end - context 'when kid is given as in a hash parameter' do + context 'when kid is given in a hash parameter' do it 'uses the given kid' do expect(described_class.new(OpenSSL::PKey::RSA.new(2048), kid: 'given').kid).to eq('given') end end + + context 'when kid is given as a common JWK parameter' do + it 'uses the given kid' do + expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { kid: 'given' }).kid).to eq('given') + end + end + end + + describe '.common_parameters' do + context 'when a common parameters hash is given' do + it 'imports the common parameter' do + expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { use: 'sig' }).common_parameters).to include(:use) + end + + it 'converts string keys to symbol keys' do + expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { 'use' => 'sig' }).common_parameters).to include(:use) + end + end end describe '.import' do diff --git a/spec/jwk_spec.rb b/spec/jwk_spec.rb index 3d3543f0..358d3f2c 100644 --- a/spec/jwk_spec.rb +++ b/spec/jwk_spec.rb @@ -38,11 +38,18 @@ expect(subject.export).to eq(params) end end + + context 'when a common JWK parameter is specified' do + it 'returns the defined common JWK parameter' do + params[:use] = 'sig' + expect(subject.export).to eq(params) + end + end end describe '.new' do - let(:kid) { nil } - subject { described_class.new(keypair, kid) } + let(:options) { nil } + subject { described_class.new(keypair, options) } context 'when RSA key is given' do let(:keypair) { rsa_key } @@ -61,9 +68,17 @@ context 'when kid is given' do let(:keypair) { rsa_key } - let(:kid) { 'CUSTOM_KID' } + let(:options) { 'CUSTOM_KID' } it 'sets the kid' do - expect(subject.kid).to eq(kid) + expect(subject.kid).to eq(options) + end + end + + context 'when a common parameter is given' do + let(:keypair) { rsa_key } + let(:options) { { common_parameters: { 'use' => 'sig' } } } + it 'sets the common parameter' do + expect(subject.common_parameters).to eq({ use: 'sig' }) end end end From b382024721767b760e6377df8336c6b2b05decdb Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Fri, 7 Oct 2022 17:06:21 +0200 Subject: [PATCH 02/15] JWK: Enforcement of a corresponding kty value for imports --- lib/jwt/jwk/ec.rb | 3 ++- lib/jwt/jwk/hmac.rb | 5 +++-- lib/jwt/jwk/rsa.rb | 20 +++++++++----------- spec/jwk/rsa_spec.rb | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index 83a32de1..089f79bf 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -95,12 +95,13 @@ def import(jwk_data) # See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an # explanation of the relevant parameters. parameters = jwk_data.transform_keys(&:to_sym) - parameters.delete(:kty) # Will be re-added upon export + jwk_kty = parameters.delete(:kty) # Will be re-added upon export jwk_crv = parameters.delete(:crv) jwk_x = parameters.delete(:x) jwk_y = parameters.delete(:y) jwk_d = parameters.delete(:d) + raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), common_parameters: parameters) diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index e7e2c8f0..6048c942 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -55,9 +55,10 @@ def key_digest class << self def import(jwk_data) parameters = jwk_data.transform_keys(&:to_sym) - parameters.delete(:kty) # Will be re-added upon export - jwk_k = parameters.delete(:k) + jwk_kty = parameters.delete(:kty) # Will be re-added upon export + jwk_k = parameters.delete(:k) + raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k new(jwk_k, common_parameters: parameters) diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index b4d0fcf5..f4d64f5c 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -14,7 +14,7 @@ def initialize(keypair, options = {}) raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA) @keypair = keypair - + super(options) end @@ -72,8 +72,12 @@ def import(jwk_data) decode_open_ssl_bn(value) end parameters = jwk_data.transform_keys(&:to_sym) - parameters.delete(:kty) # Will be re-added upon export + jwk_kty = parameters.delete(:kty) # Will be re-added upon export RSA_KEY_ELEMENTS.each { |e| parameters.delete e } + + raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY + raise JWT::JWKError, 'Key format is invalid for RSA' unless pkey_params[:n] && pkey_params[:e] + new(rsa_pkey(pkey_params), common_parameters: parameters) end @@ -87,15 +91,9 @@ def jwk_attributes(jwk_data, *attributes) end end - def rsa_pkey(rsa_parameters) - raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e] - - create_rsa_key(rsa_parameters) - end - if ::JWT.openssl_3? ASN1_SEQUENCE = %i[n e d p q dp dq qi].freeze - def create_rsa_key(rsa_parameters) + def rsa_pkey(rsa_parameters) sequence = ASN1_SEQUENCE.each_with_object([]) do |key, arr| next if rsa_parameters[key].nil? @@ -109,7 +107,7 @@ def create_rsa_key(rsa_parameters) OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der) end elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key) - def create_rsa_key(rsa_parameters) + def rsa_pkey(rsa_parameters) OpenSSL::PKey::RSA.new.tap do |rsa_key| rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d]) rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q] @@ -117,7 +115,7 @@ def create_rsa_key(rsa_parameters) end end else - def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize + def rsa_pkey(rsa_parameters) # rubocop:disable Metrics/AbcSize OpenSSL::PKey::RSA.new.tap do |rsa_key| rsa_key.n = rsa_parameters[:n] rsa_key.e = rsa_parameters[:e] diff --git a/spec/jwk/rsa_spec.rb b/spec/jwk/rsa_spec.rb index d04bc1e4..1339eecd 100644 --- a/spec/jwk/rsa_spec.rb +++ b/spec/jwk/rsa_spec.rb @@ -108,7 +108,7 @@ let(:exported_key) { described_class.new(rsa_key).export } context 'when keypair is imported with symbol keys' do - let(:params) { { e: exported_key[:e], n: exported_key[:n] } } + let(:params) { { kty: 'RSA', e: exported_key[:e], n: exported_key[:n] } } it 'returns a hash with the public parts of the key' do expect(subject).to be_a described_class expect(subject.private?).to eq false @@ -117,7 +117,7 @@ end context 'when keypair is imported with string keys from JSON' do - let(:params) { { 'e' => exported_key[:e], 'n' => exported_key[:n] } } + let(:params) { { 'kty' => 'RSA', 'e' => exported_key[:e], 'n' => exported_key[:n] } } it 'returns a hash with the public parts of the key' do expect(subject).to be_a described_class expect(subject.private?).to eq false From aee7d1deb0ceb891bde227ee0b6632c7bffd8c21 Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Fri, 7 Oct 2022 17:17:18 +0200 Subject: [PATCH 03/15] Rubocop linting changes --- lib/jwt/jwk/key_base.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/jwt/jwk/key_base.rb b/lib/jwt/jwk/key_base.rb index 1385bf6a..a24fb7be 100644 --- a/lib/jwt/jwk/key_base.rb +++ b/lib/jwt/jwk/key_base.rb @@ -9,14 +9,16 @@ def self.inherited(klass) end def initialize(options) - options = options&.clone || {} # Beware: Only a shallow copy + options ||= {} if options.is_a?(String) # For backwards compatibility when kid was a String options = { kid: options } end - @common_parameters = options[:common_parameters]&.transform_keys(&:to_sym) || {} + @common_parameters = options[:common_parameters] || {} + @common_parameters = @common_parameters.transform_keys(&:to_sym) # Uniform interface @common_parameters[:kid] = options[:kid] if options[:kid] # kid can be specified outside common_parameters + @kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator end From ce59f8631a2a9585480d205a1746b44d11651a30 Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Mon, 10 Oct 2022 09:50:28 +0200 Subject: [PATCH 04/15] JWK: Generate `kid` upon initialization --- lib/jwt/jwk/ec.rb | 1 - lib/jwt/jwk/hmac.rb | 5 +---- lib/jwt/jwk/key_base.rb | 16 ++++++++++------ lib/jwt/jwk/rsa.rb | 1 - spec/jwk_spec.rb | 3 ++- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index 089f79bf..54a16c4c 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -37,7 +37,6 @@ def members end def export(options = {}) - kid # Make sure a kid is generated exported_hash = common_parameters.merge(members) return exported_hash unless private? && options[:include_private] == true diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index 6048c942..98585f6d 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -25,10 +25,7 @@ def public_key # See https://tools.ietf.org/html/rfc7517#appendix-A.3 def export(options = {}) - exported_hash = common_parameters.merge({ - kty: KTY, - kid: kid - }) + exported_hash = common_parameters.merge({ kty: KTY }) return exported_hash unless private? && options[:include_private] == true diff --git a/lib/jwt/jwk/key_base.rb b/lib/jwt/jwk/key_base.rb index a24fb7be..de7ba799 100644 --- a/lib/jwt/jwk/key_base.rb +++ b/lib/jwt/jwk/key_base.rb @@ -17,23 +17,27 @@ def initialize(options) @common_parameters = options[:common_parameters] || {} @common_parameters = @common_parameters.transform_keys(&:to_sym) # Uniform interface - @common_parameters[:kid] = options[:kid] if options[:kid] # kid can be specified outside common_parameters - @kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator + initialize_kid(options) end def kid - @common_parameters[:kid] ||= generate_kid + @common_parameters[:kid] end attr_accessor :common_parameters private - attr_reader :kid_generator + def initialize_kid(options) + # kid can be specified outside common_parameters, takes priority + @common_parameters[:kid] = options[:kid] if options[:kid] - def generate_kid - kid_generator.new(self).generate + return if @common_parameters[:kid] + + # No kid given. Generate one from the public key + kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator + @common_parameters[:kid] = kid_generator.new(self).generate end end end diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index f4d64f5c..099a41d9 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -27,7 +27,6 @@ def public_key end def export(options = {}) - kid # Make sure a kid is generated exported_hash = common_parameters.merge(members) return exported_hash unless private? && options[:include_private] == true diff --git a/spec/jwk_spec.rb b/spec/jwk_spec.rb index 358d3f2c..b821426e 100644 --- a/spec/jwk_spec.rb +++ b/spec/jwk_spec.rb @@ -78,7 +78,8 @@ let(:keypair) { rsa_key } let(:options) { { common_parameters: { 'use' => 'sig' } } } it 'sets the common parameter' do - expect(subject.common_parameters).to eq({ use: 'sig' }) + expect(subject.common_parameters).to include(:use) + expect(subject.common_parameters[:use]).to eq('sig') end end end From 57fb464d7e34a5a38b53d6f043f1c4a2c97189cd Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Mon, 10 Oct 2022 13:04:02 +0200 Subject: [PATCH 05/15] JWK: Replaced `common_parameters` member with key accessor --- lib/jwt/jwk/ec.rb | 17 +++++++++++++++++ lib/jwt/jwk/hmac.rb | 16 ++++++++++++++++ lib/jwt/jwk/key_base.rb | 18 +++++++++++++----- lib/jwt/jwk/rsa.rb | 20 ++++++++++++++++++-- spec/jwk/hmac_spec.rb | 2 +- spec/jwk/rsa_spec.rb | 4 ++-- spec/jwk_spec.rb | 22 ++++++++++++++++++++-- 7 files changed, 87 insertions(+), 12 deletions(-) diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index 54a16c4c..ad4987fc 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -11,6 +11,7 @@ class EC < KeyBase # rubocop:disable Metrics/ClassLength KTY = 'EC' KTYS = [KTY, OpenSSL::PKey::EC].freeze BINARY = 2 + EC_KEY_ELEMENTS = %i[crv x y d].freeze attr_reader :keypair @@ -51,6 +52,22 @@ def key_digest OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) end + def [](key) + if EC_KEY_ELEMENTS.include?(key) || key.to_sym == :kty + raise ArgumentError, 'cannot access cryptographic key attributes' + end + + method(__method__).super_method.call(key) + end + + def []=(key, value) + if EC_KEY_ELEMENTS.include?(key) || key.to_sym == :kty + raise ArgumentError, 'cannot access cryptographic key attributes' + end + + method(__method__).super_method.call(key, value) + end + private def append_private_parts(the_hash) diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index 98585f6d..9e1f0439 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -49,6 +49,22 @@ def key_digest OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) end + def [](key) + if key.to_sym == :k || key.to_sym == :kty + raise ArgumentError, 'cannot access cryptographic key attributes' + end + + method(__method__).super_method.call(key) + end + + def []=(key, value) + if key.to_sym == :k || key.to_sym == :kty + raise ArgumentError, 'cannot access cryptographic key attributes' + end + + method(__method__).super_method.call(key, value) + end + class << self def import(jwk_data) parameters = jwk_data.transform_keys(&:to_sym) diff --git a/lib/jwt/jwk/key_base.rb b/lib/jwt/jwk/key_base.rb index de7ba799..1b682562 100644 --- a/lib/jwt/jwk/key_base.rb +++ b/lib/jwt/jwk/key_base.rb @@ -22,22 +22,30 @@ def initialize(options) end def kid - @common_parameters[:kid] + self[:kid] end - attr_accessor :common_parameters + def [](key) + @common_parameters[key.to_sym] + end + + def []=(key, value) + @common_parameters[key.to_sym] = value + end private + attr_reader :common_parameters + def initialize_kid(options) # kid can be specified outside common_parameters, takes priority - @common_parameters[:kid] = options[:kid] if options[:kid] + self[:kid] = options[:kid] if options[:kid] - return if @common_parameters[:kid] + return if self[:kid] # No kid given. Generate one from the public key kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator - @common_parameters[:kid] = kid_generator.new(self).generate + self[:kid] = kid_generator.new(self).generate end end end diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index 099a41d9..acb9b1c3 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -2,7 +2,7 @@ module JWT module JWK - class RSA < KeyBase + class RSA < KeyBase # rubocop:disable Metrics/ClassLength BINARY = 2 KTY = 'RSA' KTYS = [KTY, OpenSSL::PKey::RSA].freeze @@ -48,6 +48,22 @@ def key_digest OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) end + def [](key) + if RSA_KEY_ELEMENTS.include?(key) || key.to_sym == :kty + raise ArgumentError, 'cannot access cryptographic key attributes' + end + + method(__method__).super_method.call(key) + end + + def []=(key, value) + if RSA_KEY_ELEMENTS.include?(key) || key.to_sym == :kty + raise ArgumentError, 'cannot access cryptographic key attributes' + end + + method(__method__).super_method.call(key, value) + end + private def append_private_parts(the_hash) @@ -72,7 +88,7 @@ def import(jwk_data) end parameters = jwk_data.transform_keys(&:to_sym) jwk_kty = parameters.delete(:kty) # Will be re-added upon export - RSA_KEY_ELEMENTS.each { |e| parameters.delete e } + RSA_KEY_ELEMENTS.each { |e| parameters.delete(e) } raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY raise JWT::JWKError, 'Key format is invalid for RSA' unless pkey_params[:n] && pkey_params[:e] diff --git a/spec/jwk/hmac_spec.rb b/spec/jwk/hmac_spec.rb index b04ae709..ae2c3f27 100644 --- a/spec/jwk/hmac_spec.rb +++ b/spec/jwk/hmac_spec.rb @@ -64,7 +64,7 @@ super().merge(use: 'sig') } it 'imports that common parameter' do - expect(subject.common_parameters).to include(:use) + expect(subject[:use]).to eq('sig') end end end diff --git a/spec/jwk/rsa_spec.rb b/spec/jwk/rsa_spec.rb index 1339eecd..2686f19d 100644 --- a/spec/jwk/rsa_spec.rb +++ b/spec/jwk/rsa_spec.rb @@ -94,11 +94,11 @@ describe '.common_parameters' do context 'when a common parameters hash is given' do it 'imports the common parameter' do - expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { use: 'sig' }).common_parameters).to include(:use) + expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { use: 'sig' })[:use]).to eq('sig') end it 'converts string keys to symbol keys' do - expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { 'use' => 'sig' }).common_parameters).to include(:use) + expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { 'use' => 'sig' })[:use]).to eq('sig') end end end diff --git a/spec/jwk_spec.rb b/spec/jwk_spec.rb index b821426e..8cadd7f8 100644 --- a/spec/jwk_spec.rb +++ b/spec/jwk_spec.rb @@ -78,9 +78,27 @@ let(:keypair) { rsa_key } let(:options) { { common_parameters: { 'use' => 'sig' } } } it 'sets the common parameter' do - expect(subject.common_parameters).to include(:use) - expect(subject.common_parameters[:use]).to eq('sig') + expect(subject[:use]).to eq('sig') end end end + + describe '.[]' do + let(:options) { { common_parameters: { use: 'sig' } } } + let(:keypair) { rsa_key } + subject { described_class.new(keypair, options) } + + it 'allows to read common parameters via the key-accessor' do + expect(subject.export[:use]).to eq('sig') + end + + it 'allows to set common parameters via the key-accessor' do + subject[:use] = 'enc' + expect(subject.export[:use]).to eq('enc') + end + + it 'rejects key parameters as keys via the key-accessor' do + expect { subject[:p] }.to raise_error(ArgumentError) + end + end end From 1898cb4ca7d0b8bb7f23dfeffc4c13a0571af5f8 Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Mon, 10 Oct 2022 13:49:51 +0200 Subject: [PATCH 06/15] JWK: Readability improvements --- lib/jwt/jwk/ec.rb | 10 +++++----- lib/jwt/jwk/hmac.rb | 4 ++-- lib/jwt/jwk/rsa.rb | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index ad4987fc..9ef28f9d 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -57,7 +57,7 @@ def [](key) raise ArgumentError, 'cannot access cryptographic key attributes' end - method(__method__).super_method.call(key) + super(key) end def []=(key, value) @@ -65,7 +65,7 @@ def []=(key, value) raise ArgumentError, 'cannot access cryptographic key attributes' end - method(__method__).super_method.call(key, value) + super(key, value) end private @@ -120,7 +120,7 @@ def import(jwk_data) raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y - new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), common_parameters: parameters) + new(create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d), common_parameters: parameters) end def to_openssl_curve(crv) @@ -139,7 +139,7 @@ def to_openssl_curve(crv) private if ::JWT.openssl_3? - def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength + def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength curve = to_openssl_curve(jwk_crv) x_octets = decode_octets(jwk_x) @@ -175,7 +175,7 @@ def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength OpenSSL::PKey::EC.new(sequence.to_der) end else - def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) + def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) curve = to_openssl_curve(jwk_crv) x_octets = decode_octets(jwk_x) diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index 9e1f0439..42b3573f 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -54,7 +54,7 @@ def [](key) raise ArgumentError, 'cannot access cryptographic key attributes' end - method(__method__).super_method.call(key) + super(key) end def []=(key, value) @@ -62,7 +62,7 @@ def []=(key, value) raise ArgumentError, 'cannot access cryptographic key attributes' end - method(__method__).super_method.call(key, value) + super(key, value) end class << self diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index acb9b1c3..3ba48c8d 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -53,7 +53,7 @@ def [](key) raise ArgumentError, 'cannot access cryptographic key attributes' end - method(__method__).super_method.call(key) + super(key) end def []=(key, value) @@ -61,7 +61,7 @@ def []=(key, value) raise ArgumentError, 'cannot access cryptographic key attributes' end - method(__method__).super_method.call(key, value) + super(key, value) end private @@ -93,7 +93,7 @@ def import(jwk_data) raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY raise JWT::JWKError, 'Key format is invalid for RSA' unless pkey_params[:n] && pkey_params[:e] - new(rsa_pkey(pkey_params), common_parameters: parameters) + new(create_rsa_key(pkey_params), common_parameters: parameters) end private @@ -108,7 +108,7 @@ def jwk_attributes(jwk_data, *attributes) if ::JWT.openssl_3? ASN1_SEQUENCE = %i[n e d p q dp dq qi].freeze - def rsa_pkey(rsa_parameters) + def create_rsa_key(rsa_parameters) sequence = ASN1_SEQUENCE.each_with_object([]) do |key, arr| next if rsa_parameters[key].nil? @@ -122,7 +122,7 @@ def rsa_pkey(rsa_parameters) OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der) end elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key) - def rsa_pkey(rsa_parameters) + def create_rsa_key(rsa_parameters) OpenSSL::PKey::RSA.new.tap do |rsa_key| rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d]) rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q] @@ -130,7 +130,7 @@ def rsa_pkey(rsa_parameters) end end else - def rsa_pkey(rsa_parameters) # rubocop:disable Metrics/AbcSize + def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize OpenSSL::PKey::RSA.new.tap do |rsa_key| rsa_key.n = rsa_parameters[:n] rsa_key.e = rsa_parameters[:e] From 62d7ce8039b6931a9aa1123aed2f25928d0ee4c5 Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Tue, 11 Oct 2022 14:56:54 +0200 Subject: [PATCH 07/15] JWK: Keypairs now generated on demand --- lib/jwt/jwk.rb | 4 +- lib/jwt/jwk/ec.rb | 239 +++++++++++++++++++--------------------- lib/jwt/jwk/hmac.rb | 64 +++++------ lib/jwt/jwk/key_base.rb | 12 +- lib/jwt/jwk/rsa.rb | 177 ++++++++++++++--------------- spec/jwk/ec_spec.rb | 2 +- spec/jwk/rsa_spec.rb | 6 +- spec/jwk_spec.rb | 13 ++- 8 files changed, 246 insertions(+), 271 deletions(-) diff --git a/lib/jwt/jwk.rb b/lib/jwt/jwk.rb index 1db2060e..f4970053 100644 --- a/lib/jwt/jwk.rb +++ b/lib/jwt/jwk.rb @@ -14,10 +14,10 @@ def import(jwk_data) end.import(jwk_data) end - def create_from(keypair, kid = nil) + def create_from(keypair, options = nil, params = {}) mappings.fetch(keypair.class) do |klass| raise JWT::JWKError, "Cannot create JWK from a #{klass.name}" - end.new(keypair, kid) + end.new(keypair, options, params) end def classes diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index 9ef28f9d..3c53d61b 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -11,38 +11,44 @@ class EC < KeyBase # rubocop:disable Metrics/ClassLength KTY = 'EC' KTYS = [KTY, OpenSSL::PKey::EC].freeze BINARY = 2 - EC_KEY_ELEMENTS = %i[crv x y d].freeze + EC_PUBLIC_KEY_ELEMENTS = %i[kty crv x y].freeze + EC_PRIVATE_KEY_ELEMENTS = %i[d].freeze + EC_KEY_ELEMENTS = (EC_PRIVATE_KEY_ELEMENTS + EC_PUBLIC_KEY_ELEMENTS).freeze - attr_reader :keypair + def initialize(keypair, options = nil, params = {}) + options ||= {} - def initialize(keypair, options = {}) - raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC) + if keypair.is_a?(OpenSSL::PKey::EC) + keypair = parse_ec_key(keypair) + end + + raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(Hash) + + keypair = keypair.transform_keys(&:to_sym) + params = params.transform_keys(&:to_sym) + raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty? + raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY + raise JWT::JWKError, 'Key format is invalid for EC' unless keypair[:crv] && keypair[:x] && keypair[:y] - @keypair = keypair + super(options, keypair.merge(params)) + end - super(options) + def keypair + create_ec_key(self[:crv], self[:x], self[:y], self[:d]) end def private? - @keypair.private_key? + keypair.private_key? end def members - crv, x_octets, y_octets = keypair_components(keypair) - { - kty: KTY, - crv: crv, - x: encode_octets(x_octets), - y: encode_octets(y_octets) - } + EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] } end def export(options = {}) - exported_hash = common_parameters.merge(members) - - return exported_hash unless private? && options[:include_private] == true - - append_private_parts(exported_hash) + exported = parameters.clone + exported = exported.except(*EC_PRIVATE_KEY_ELEMENTS) unless private? && options[:include_private] == true + exported end def key_digest @@ -52,17 +58,9 @@ def key_digest OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) end - def [](key) - if EC_KEY_ELEMENTS.include?(key) || key.to_sym == :kty - raise ArgumentError, 'cannot access cryptographic key attributes' - end - - super(key) - end - def []=(key, value) - if EC_KEY_ELEMENTS.include?(key) || key.to_sym == :kty - raise ArgumentError, 'cannot access cryptographic key attributes' + if EC_KEY_ELEMENTS.include?(key) + raise ArgumentError, 'cannot overwrite cryptographic key attributes' end super(key, value) @@ -70,13 +68,6 @@ def []=(key, value) private - def append_private_parts(the_hash) - octets = keypair.private_key.to_bn.to_s(BINARY) - the_hash.merge( - d: encode_octets(octets) - ) - end - def keypair_components(ec_keypair) encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY) case ec_keypair.group.curve_name @@ -99,6 +90,8 @@ def keypair_components(ec_keypair) end def encode_octets(octets) + return unless octets + ::JWT::Base64.url_encode(octets) end @@ -106,21 +99,94 @@ def encode_open_ssl_bn(key_part) ::JWT::Base64.url_encode(key_part.to_s(BINARY)) end + def parse_ec_key(key) + crv, x_octets, y_octets = keypair_components(key) + octets = key.private_key&.to_bn&.to_s(BINARY) + { + kty: KTY, + crv: crv, + x: encode_octets(x_octets), + y: encode_octets(y_octets), + d: encode_octets(octets) + }.compact + end + + if ::JWT.openssl_3? + def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength + curve = EC.to_openssl_curve(jwk_crv) + + x_octets = decode_octets(jwk_x) + y_octets = decode_octets(jwk_y) + + point = OpenSSL::PKey::EC::Point.new( + OpenSSL::PKey::EC::Group.new(curve), + OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2) + ) + + sequence = if jwk_d + # https://datatracker.ietf.org/doc/html/rfc5915.html + # ECPrivateKey ::= SEQUENCE { + # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + # privateKey OCTET STRING, + # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, + # publicKey [1] BIT STRING OPTIONAL + # } + + OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Integer(1), + OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)), + OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT), + OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT) + ]) + else + OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]), + OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed)) + ]) + end + + OpenSSL::PKey::EC.new(sequence.to_der) + end + else + def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) + curve = EC.to_openssl_curve(jwk_crv) + + x_octets = decode_octets(jwk_x) + y_octets = decode_octets(jwk_y) + + key = OpenSSL::PKey::EC.new(curve) + + # The details of the `Point` instantiation are covered in: + # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html + # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html + # - https://tools.ietf.org/html/rfc5480#section-2.2 + # - https://www.secg.org/SEC1-Ver-1.0.pdf + # Section 2.3.3 of the last of these references specifies that the + # encoding of an uncompressed point consists of the byte `0x04` followed + # by the x value then the y value. + point = OpenSSL::PKey::EC::Point.new( + OpenSSL::PKey::EC::Group.new(curve), + OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2) + ) + + key.public_key = point + key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d + + key + end + end + + def decode_octets(jwk_data) + ::JWT::Base64.url_decode(jwk_data) + end + + def decode_open_ssl_bn(jwk_data) + OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY) + end + class << self def import(jwk_data) - # See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an - # explanation of the relevant parameters. - parameters = jwk_data.transform_keys(&:to_sym) - jwk_kty = parameters.delete(:kty) # Will be re-added upon export - jwk_crv = parameters.delete(:crv) - jwk_x = parameters.delete(:x) - jwk_y = parameters.delete(:y) - jwk_d = parameters.delete(:d) - - raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY - raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y - - new(create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d), common_parameters: parameters) + new(jwk_data) end def to_openssl_curve(crv) @@ -135,81 +201,6 @@ def to_openssl_curve(crv) else raise JWT::JWKError, 'Invalid curve provided' end end - - private - - if ::JWT.openssl_3? - def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength - curve = to_openssl_curve(jwk_crv) - - x_octets = decode_octets(jwk_x) - y_octets = decode_octets(jwk_y) - - point = OpenSSL::PKey::EC::Point.new( - OpenSSL::PKey::EC::Group.new(curve), - OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2) - ) - - sequence = if jwk_d - # https://datatracker.ietf.org/doc/html/rfc5915.html - # ECPrivateKey ::= SEQUENCE { - # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), - # privateKey OCTET STRING, - # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, - # publicKey [1] BIT STRING OPTIONAL - # } - - OpenSSL::ASN1::Sequence([ - OpenSSL::ASN1::Integer(1), - OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)), - OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT), - OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT) - ]) - else - OpenSSL::ASN1::Sequence([ - OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]), - OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed)) - ]) - end - - OpenSSL::PKey::EC.new(sequence.to_der) - end - else - def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) - curve = to_openssl_curve(jwk_crv) - - x_octets = decode_octets(jwk_x) - y_octets = decode_octets(jwk_y) - - key = OpenSSL::PKey::EC.new(curve) - - # The details of the `Point` instantiation are covered in: - # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html - # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html - # - https://tools.ietf.org/html/rfc5480#section-2.2 - # - https://www.secg.org/SEC1-Ver-1.0.pdf - # Section 2.3.3 of the last of these references specifies that the - # encoding of an uncompressed point consists of the byte `0x04` followed - # by the x value then the y value. - point = OpenSSL::PKey::EC::Point.new( - OpenSSL::PKey::EC::Group.new(curve), - OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2) - ) - - key.public_key = point - key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d - - key - end - end - - def decode_octets(jwk_data) - ::JWT::Base64.url_decode(jwk_data) - end - - def decode_open_ssl_bn(jwk_data) - OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY) - end end end end diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index 42b3573f..6b32913b 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -5,14 +5,30 @@ module JWK class HMAC < KeyBase KTY = 'oct' KTYS = [KTY, String].freeze + HMAC_PUBLIC_KEY_ELEMENTS = %i[kty].freeze + HMAC_PRIVATE_KEY_ELEMENTS = %i[k].freeze + HMAC_KEY_ELEMENTS = (HMAC_PRIVATE_KEY_ELEMENTS + HMAC_PUBLIC_KEY_ELEMENTS).freeze - attr_reader :signing_key + def initialize(keypair, options = nil, params = {}) + options ||= {} - def initialize(signing_key, options = {}) - raise ArgumentError, 'signing_key must be of type String' unless signing_key.is_a?(String) + if keypair.is_a?(String) + keypair = { kty: KTY, k: keypair } + end + + raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(Hash) - @signing_key = signing_key - super(options) + keypair = keypair.transform_keys(&:to_sym) + params = params.transform_keys(&:to_sym) + raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty? + raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY + raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k] + + super(options, keypair.merge(params)) + end + + def keypair + self[:k] end def private? @@ -25,23 +41,16 @@ def public_key # See https://tools.ietf.org/html/rfc7517#appendix-A.3 def export(options = {}) - exported_hash = common_parameters.merge({ kty: KTY }) - - return exported_hash unless private? && options[:include_private] == true - - exported_hash.merge( - k: signing_key - ) + exported = parameters.clone + exported = exported.except(*HMAC_PRIVATE_KEY_ELEMENTS) unless private? && options[:include_private] == true + exported end def members - { - kty: KTY, - k: signing_key - } + HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] } end - alias keypair signing_key # for backwards compatibility + alias signing_key keypair # for backwards compatibility def key_digest sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key), @@ -49,17 +58,9 @@ def key_digest OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) end - def [](key) - if key.to_sym == :k || key.to_sym == :kty - raise ArgumentError, 'cannot access cryptographic key attributes' - end - - super(key) - end - def []=(key, value) - if key.to_sym == :k || key.to_sym == :kty - raise ArgumentError, 'cannot access cryptographic key attributes' + if HMAC_KEY_ELEMENTS.include?(key) + raise ArgumentError, 'cannot overwrite cryptographic key attributes' end super(key, value) @@ -67,14 +68,7 @@ def []=(key, value) class << self def import(jwk_data) - parameters = jwk_data.transform_keys(&:to_sym) - jwk_kty = parameters.delete(:kty) # Will be re-added upon export - jwk_k = parameters.delete(:k) - - raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY - raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k - - new(jwk_k, common_parameters: parameters) + new(jwk_data) end end end diff --git a/lib/jwt/jwk/key_base.rb b/lib/jwt/jwk/key_base.rb index 1b682562..eb1cb571 100644 --- a/lib/jwt/jwk/key_base.rb +++ b/lib/jwt/jwk/key_base.rb @@ -8,16 +8,14 @@ def self.inherited(klass) ::JWT::JWK.classes << klass end - def initialize(options) + def initialize(options, params = {}) options ||= {} if options.is_a?(String) # For backwards compatibility when kid was a String options = { kid: options } end - @common_parameters = options[:common_parameters] || {} - @common_parameters = @common_parameters.transform_keys(&:to_sym) # Uniform interface - + @parameters = params.transform_keys(&:to_sym) # Uniform interface initialize_kid(options) end @@ -26,16 +24,16 @@ def kid end def [](key) - @common_parameters[key.to_sym] + @parameters[key.to_sym] end def []=(key, value) - @common_parameters[key.to_sym] = value + @parameters[key.to_sym] = value end private - attr_reader :common_parameters + attr_reader :parameters def initialize_kid(options) # kid can be specified outside common_parameters, takes priority diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index 3ba48c8d..7a744c31 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -2,20 +2,34 @@ module JWT module JWK - class RSA < KeyBase # rubocop:disable Metrics/ClassLength + class RSA < KeyBase BINARY = 2 KTY = 'RSA' KTYS = [KTY, OpenSSL::PKey::RSA].freeze - RSA_KEY_ELEMENTS = %i[n e d p q dp dq qi].freeze + RSA_PUBLIC_KEY_ELEMENTS = %i[kty n e].freeze + RSA_PRIVATE_KEY_ELEMENTS = %i[d p q dp dq qi].freeze + RSA_KEY_ELEMENTS = (RSA_PRIVATE_KEY_ELEMENTS + RSA_PUBLIC_KEY_ELEMENTS).freeze - attr_reader :keypair + def initialize(keypair, options = nil, params = {}) + options ||= {} - def initialize(keypair, options = {}) - raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA) + if keypair.is_a?(OpenSSL::PKey::RSA) + keypair = parse_rsa_key(keypair) + end + + raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(Hash) - @keypair = keypair + keypair = keypair.transform_keys(&:to_sym) + params = params.transform_keys(&:to_sym) + raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (RSA_KEY_ELEMENTS & params.keys).empty? + raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY + raise JWT::JWKError, 'Key format is invalid for RSA' unless keypair[:n] && keypair[:e] - super(options) + super(options, keypair.merge(params)) + end + + def keypair + create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty]))) end def private? @@ -27,19 +41,13 @@ def public_key end def export(options = {}) - exported_hash = common_parameters.merge(members) - - return exported_hash unless private? && options[:include_private] == true - - append_private_parts(exported_hash) + exported = parameters.clone + exported = exported.except(*RSA_PRIVATE_KEY_ELEMENTS) unless private? && options[:include_private] == true + exported end def members - { - kty: KTY, - n: encode_open_ssl_bn(public_key.n), - e: encode_open_ssl_bn(public_key.e) - } + RSA_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] } end def key_digest @@ -48,17 +56,9 @@ def key_digest OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) end - def [](key) - if RSA_KEY_ELEMENTS.include?(key) || key.to_sym == :kty - raise ArgumentError, 'cannot access cryptographic key attributes' - end - - super(key) - end - def []=(key, value) - if RSA_KEY_ELEMENTS.include?(key) || key.to_sym == :kty - raise ArgumentError, 'cannot access cryptographic key attributes' + if RSA_KEY_ELEMENTS.include?(key) + raise ArgumentError, 'cannot overwrite cryptographic key attributes' end super(key, value) @@ -66,88 +66,79 @@ def []=(key, value) private - def append_private_parts(the_hash) - the_hash.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) - ) + def parse_rsa_key(key) + { + kty: KTY, + n: encode_open_ssl_bn(key.n), + e: encode_open_ssl_bn(key.e), + d: encode_open_ssl_bn(key.d), + p: encode_open_ssl_bn(key.p), + q: encode_open_ssl_bn(key.q), + dp: encode_open_ssl_bn(key.dmp1), + dq: encode_open_ssl_bn(key.dmq1), + qi: encode_open_ssl_bn(key.iqmp) + }.compact + end + + def jwk_attributes(*attributes) + attributes.each_with_object({}) do |attribute, hash| + hash[attribute] = decode_open_ssl_bn(self[attribute]) + end end def encode_open_ssl_bn(key_part) + return unless key_part + ::JWT::Base64.url_encode(key_part.to_s(BINARY)) end - class << self - def import(jwk_data) - pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value| - decode_open_ssl_bn(value) + if ::JWT.openssl_3? + ASN1_SEQUENCE = %i[n e d p q dp dq qi].freeze + def create_rsa_key(rsa_parameters) + sequence = ASN1_SEQUENCE.each_with_object([]) do |key, arr| + next if rsa_parameters[key].nil? + + arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key]) end - parameters = jwk_data.transform_keys(&:to_sym) - jwk_kty = parameters.delete(:kty) # Will be re-added upon export - RSA_KEY_ELEMENTS.each { |e| parameters.delete(e) } - raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY - raise JWT::JWKError, 'Key format is invalid for RSA' unless pkey_params[:n] && pkey_params[:e] + if sequence.size > 2 # For a private key + sequence.unshift(OpenSSL::ASN1::Integer.new(0)) + end - new(create_rsa_key(pkey_params), common_parameters: parameters) + OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der) end - - private - - def jwk_attributes(jwk_data, *attributes) - attributes.each_with_object({}) do |attribute, hash| - value = jwk_data[attribute] || jwk_data[attribute.to_s] - value = yield(value) if block_given? - hash[attribute] = value + elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key) + def create_rsa_key(rsa_parameters) + OpenSSL::PKey::RSA.new.tap do |rsa_key| + rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d]) + rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q] + rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi] end end - - if ::JWT.openssl_3? - ASN1_SEQUENCE = %i[n e d p q dp dq qi].freeze - def create_rsa_key(rsa_parameters) - sequence = ASN1_SEQUENCE.each_with_object([]) do |key, arr| - next if rsa_parameters[key].nil? - - arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key]) - end - - if sequence.size > 2 # For a private key - sequence.unshift(OpenSSL::ASN1::Integer.new(0)) - end - - OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der) - end - elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key) - def create_rsa_key(rsa_parameters) - OpenSSL::PKey::RSA.new.tap do |rsa_key| - rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d]) - rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q] - rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi] - end - end - else - def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize - OpenSSL::PKey::RSA.new.tap do |rsa_key| - rsa_key.n = rsa_parameters[:n] - rsa_key.e = rsa_parameters[:e] - rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d] - rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p] - rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q] - rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp] - rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq] - rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi] - end + else + def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize + OpenSSL::PKey::RSA.new.tap do |rsa_key| + rsa_key.n = rsa_parameters[:n] + rsa_key.e = rsa_parameters[:e] + rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d] + rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p] + rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q] + rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp] + rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq] + rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi] end end + end - def decode_open_ssl_bn(jwk_data) - return nil unless jwk_data + def decode_open_ssl_bn(jwk_data) + return nil unless jwk_data - OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY) + OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY) + end + + class << self + def import(jwk_data) + new(jwk_data) end end end diff --git a/spec/jwk/ec_spec.rb b/spec/jwk/ec_spec.rb index 0397aaf9..6dd7f6ab 100644 --- a/spec/jwk/ec_spec.rb +++ b/spec/jwk/ec_spec.rb @@ -73,7 +73,7 @@ context 'when a common parameter is given' do let(:parameters) { { use: 'sig' } } let(:keypair) { ec_key } - subject { described_class.new(keypair, common_parameters: parameters).export } + subject { described_class.new(keypair, nil, parameters).export } it 'returns a hash including the common parameter' do expect(subject).to include(:use) end diff --git a/spec/jwk/rsa_spec.rb b/spec/jwk/rsa_spec.rb index 2686f19d..967f640e 100644 --- a/spec/jwk/rsa_spec.rb +++ b/spec/jwk/rsa_spec.rb @@ -86,7 +86,7 @@ context 'when kid is given as a common JWK parameter' do it 'uses the given kid' do - expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { kid: 'given' }).kid).to eq('given') + expect(described_class.new(OpenSSL::PKey::RSA.new(2048), nil, kid: 'given').kid).to eq('given') end end end @@ -94,11 +94,11 @@ describe '.common_parameters' do context 'when a common parameters hash is given' do it 'imports the common parameter' do - expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { use: 'sig' })[:use]).to eq('sig') + expect(described_class.new(OpenSSL::PKey::RSA.new(2048), nil, use: 'sig')[:use]).to eq('sig') end it 'converts string keys to symbol keys' do - expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { 'use' => 'sig' })[:use]).to eq('sig') + expect(described_class.new(OpenSSL::PKey::RSA.new(2048), nil, { 'use' => 'sig' })[:use]).to eq('sig') end end end diff --git a/spec/jwk_spec.rb b/spec/jwk_spec.rb index 8cadd7f8..99a5093a 100644 --- a/spec/jwk_spec.rb +++ b/spec/jwk_spec.rb @@ -75,8 +75,9 @@ end context 'when a common parameter is given' do + subject { described_class.new(keypair, nil, params) } let(:keypair) { rsa_key } - let(:options) { { common_parameters: { 'use' => 'sig' } } } + let(:params) { { 'use' => 'sig' } } it 'sets the common parameter' do expect(subject[:use]).to eq('sig') end @@ -84,21 +85,21 @@ end describe '.[]' do - let(:options) { { common_parameters: { use: 'sig' } } } + let(:params) { { use: 'sig' } } let(:keypair) { rsa_key } - subject { described_class.new(keypair, options) } + subject { described_class.new(keypair, nil, params) } it 'allows to read common parameters via the key-accessor' do - expect(subject.export[:use]).to eq('sig') + expect(subject[:use]).to eq('sig') end it 'allows to set common parameters via the key-accessor' do subject[:use] = 'enc' - expect(subject.export[:use]).to eq('enc') + expect(subject[:use]).to eq('enc') end it 'rejects key parameters as keys via the key-accessor' do - expect { subject[:p] }.to raise_error(ArgumentError) + expect { subject[:kty] = 'something' }.to raise_error(ArgumentError) end end end From dc9ab94dd7522ef5e2a1791323494a036f838559 Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Wed, 12 Oct 2022 13:17:34 +0200 Subject: [PATCH 08/15] JWK: Keypair caching --- lib/jwt/jwk/ec.rb | 12 ++++++++---- lib/jwt/jwk/hmac.rb | 12 +++++++++--- lib/jwt/jwk/rsa.rb | 14 +++++++++----- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index 3c53d61b..74c71462 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -26,15 +26,13 @@ def initialize(keypair, options = nil, params = {}) keypair = keypair.transform_keys(&:to_sym) params = params.transform_keys(&:to_sym) - raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty? - raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY - raise JWT::JWKError, 'Key format is invalid for EC' unless keypair[:crv] && keypair[:x] && keypair[:y] + check_jwk(keypair, params) super(options, keypair.merge(params)) end def keypair - create_ec_key(self[:crv], self[:x], self[:y], self[:d]) + @keypair ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d]) end def private? @@ -68,6 +66,12 @@ def []=(key, value) private + def check_jwk(keypair, params) + raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty? + raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY + raise JWT::JWKError, 'Key format is invalid for EC' unless keypair[:crv] && keypair[:x] && keypair[:y] + end + def keypair_components(ec_keypair) encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY) case ec_keypair.group.curve_name diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index 6b32913b..752ca8e0 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -20,9 +20,7 @@ def initialize(keypair, options = nil, params = {}) keypair = keypair.transform_keys(&:to_sym) params = params.transform_keys(&:to_sym) - raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty? - raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY - raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k] + check_jwk(keypair, params) super(options, keypair.merge(params)) end @@ -66,6 +64,14 @@ def []=(key, value) super(key, value) end + private + + def check_jwk(keypair, params) + raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty? + raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY + raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k] + end + class << self def import(jwk_data) new(jwk_data) diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index 7a744c31..011f022f 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -2,7 +2,7 @@ module JWT module JWK - class RSA < KeyBase + class RSA < KeyBase # rubocop:disable Metrics/ClassLength BINARY = 2 KTY = 'RSA' KTYS = [KTY, OpenSSL::PKey::RSA].freeze @@ -21,15 +21,13 @@ def initialize(keypair, options = nil, params = {}) keypair = keypair.transform_keys(&:to_sym) params = params.transform_keys(&:to_sym) - raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (RSA_KEY_ELEMENTS & params.keys).empty? - raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY - raise JWT::JWKError, 'Key format is invalid for RSA' unless keypair[:n] && keypair[:e] + check_jwk(keypair, params) super(options, keypair.merge(params)) end def keypair - create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty]))) + @keypair ||= create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty]))) end def private? @@ -66,6 +64,12 @@ def []=(key, value) private + def check_jwk(keypair, params) + raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (RSA_KEY_ELEMENTS & params.keys).empty? + raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY + raise JWT::JWKError, 'Key format is invalid for RSA' unless keypair[:n] && keypair[:e] + end + def parse_rsa_key(key) { kty: KTY, From 5c158df97d33348538fed75015bc09088869e217 Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Wed, 12 Oct 2022 13:57:05 +0200 Subject: [PATCH 09/15] JWK: Swapped the initialization parameters --- lib/jwt/jwk.rb | 4 ++-- lib/jwt/jwk/ec.rb | 14 ++++++++------ lib/jwt/jwk/hmac.rb | 14 ++++++++------ lib/jwt/jwk/key_base.rb | 23 +++++++---------------- lib/jwt/jwk/rsa.rb | 14 ++++++++------ spec/jwk/ec_spec.rb | 2 +- spec/jwk/rsa_spec.rb | 10 ++-------- spec/jwk_spec.rb | 4 ++-- 8 files changed, 38 insertions(+), 47 deletions(-) diff --git a/lib/jwt/jwk.rb b/lib/jwt/jwk.rb index f4970053..10110468 100644 --- a/lib/jwt/jwk.rb +++ b/lib/jwt/jwk.rb @@ -14,10 +14,10 @@ def import(jwk_data) end.import(jwk_data) end - def create_from(keypair, options = nil, params = {}) + def create_from(keypair, params = nil, options = {}) mappings.fetch(keypair.class) do |klass| raise JWT::JWKError, "Cannot create JWK from a #{klass.name}" - end.new(keypair, options, params) + end.new(keypair, params, options) end def classes diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index 74c71462..54feff87 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -15,12 +15,14 @@ class EC < KeyBase # rubocop:disable Metrics/ClassLength EC_PRIVATE_KEY_ELEMENTS = %i[d].freeze EC_KEY_ELEMENTS = (EC_PRIVATE_KEY_ELEMENTS + EC_PUBLIC_KEY_ELEMENTS).freeze - def initialize(keypair, options = nil, params = {}) - options ||= {} + def initialize(keypair, params = nil, options = {}) + params ||= {} - if keypair.is_a?(OpenSSL::PKey::EC) - keypair = parse_ec_key(keypair) - end + # For backwards compatibility when kid was a String + params = { kid: params } if params.is_a?(String) + + # Accept OpenSSL key as input + keypair = parse_ec_key(keypair) if keypair.is_a?(OpenSSL::PKey::EC) raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(Hash) @@ -57,7 +59,7 @@ def key_digest end def []=(key, value) - if EC_KEY_ELEMENTS.include?(key) + if EC_KEY_ELEMENTS.include?(key.to_sym) raise ArgumentError, 'cannot overwrite cryptographic key attributes' end diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index 752ca8e0..a969af48 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -9,12 +9,14 @@ class HMAC < KeyBase HMAC_PRIVATE_KEY_ELEMENTS = %i[k].freeze HMAC_KEY_ELEMENTS = (HMAC_PRIVATE_KEY_ELEMENTS + HMAC_PUBLIC_KEY_ELEMENTS).freeze - def initialize(keypair, options = nil, params = {}) - options ||= {} + def initialize(keypair, params = nil, options = {}) + params ||= {} - if keypair.is_a?(String) - keypair = { kty: KTY, k: keypair } - end + # For backwards compatibility when kid was a String + params = { kid: params } if params.is_a?(String) + + # Accept String key as input + keypair = { kty: KTY, k: keypair } if keypair.is_a?(String) raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(Hash) @@ -57,7 +59,7 @@ def key_digest end def []=(key, value) - if HMAC_KEY_ELEMENTS.include?(key) + if HMAC_KEY_ELEMENTS.include?(key.to_sym) raise ArgumentError, 'cannot overwrite cryptographic key attributes' end diff --git a/lib/jwt/jwk/key_base.rb b/lib/jwt/jwk/key_base.rb index eb1cb571..7c301ca7 100644 --- a/lib/jwt/jwk/key_base.rb +++ b/lib/jwt/jwk/key_base.rb @@ -11,12 +11,14 @@ def self.inherited(klass) def initialize(options, params = {}) options ||= {} - if options.is_a?(String) # For backwards compatibility when kid was a String - options = { kid: options } - end - @parameters = params.transform_keys(&:to_sym) # Uniform interface - initialize_kid(options) + + # For backwards compatibility, kid_generator may be specified in the parameters + options[:kid_generator] ||= @parameters.delete(:kid_generator) + + # Make sure the key has a kid + kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator + self[:kid] ||= kid_generator.new(self).generate end def kid @@ -34,17 +36,6 @@ def []=(key, value) private attr_reader :parameters - - def initialize_kid(options) - # kid can be specified outside common_parameters, takes priority - self[:kid] = options[:kid] if options[:kid] - - return if self[:kid] - - # No kid given. Generate one from the public key - kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator - self[:kid] = kid_generator.new(self).generate - end end end end diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index 011f022f..8c9f4d1e 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -10,12 +10,14 @@ class RSA < KeyBase # rubocop:disable Metrics/ClassLength RSA_PRIVATE_KEY_ELEMENTS = %i[d p q dp dq qi].freeze RSA_KEY_ELEMENTS = (RSA_PRIVATE_KEY_ELEMENTS + RSA_PUBLIC_KEY_ELEMENTS).freeze - def initialize(keypair, options = nil, params = {}) - options ||= {} + def initialize(keypair, params = nil, options = {}) + params ||= {} - if keypair.is_a?(OpenSSL::PKey::RSA) - keypair = parse_rsa_key(keypair) - end + # For backwards compatibility when kid was a String + params = { kid: params } if params.is_a?(String) + + # Accept OpenSSL key as input + keypair = parse_rsa_key(keypair) if keypair.is_a?(OpenSSL::PKey::RSA) raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(Hash) @@ -55,7 +57,7 @@ def key_digest end def []=(key, value) - if RSA_KEY_ELEMENTS.include?(key) + if RSA_KEY_ELEMENTS.include?(key.to_sym) raise ArgumentError, 'cannot overwrite cryptographic key attributes' end diff --git a/spec/jwk/ec_spec.rb b/spec/jwk/ec_spec.rb index 6dd7f6ab..2ff5f01b 100644 --- a/spec/jwk/ec_spec.rb +++ b/spec/jwk/ec_spec.rb @@ -73,7 +73,7 @@ context 'when a common parameter is given' do let(:parameters) { { use: 'sig' } } let(:keypair) { ec_key } - subject { described_class.new(keypair, nil, parameters).export } + subject { described_class.new(keypair, parameters).export } it 'returns a hash including the common parameter' do expect(subject).to include(:use) end diff --git a/spec/jwk/rsa_spec.rb b/spec/jwk/rsa_spec.rb index 967f640e..686a28f3 100644 --- a/spec/jwk/rsa_spec.rb +++ b/spec/jwk/rsa_spec.rb @@ -83,22 +83,16 @@ expect(described_class.new(OpenSSL::PKey::RSA.new(2048), kid: 'given').kid).to eq('given') end end - - context 'when kid is given as a common JWK parameter' do - it 'uses the given kid' do - expect(described_class.new(OpenSSL::PKey::RSA.new(2048), nil, kid: 'given').kid).to eq('given') - end - end end describe '.common_parameters' do context 'when a common parameters hash is given' do it 'imports the common parameter' do - expect(described_class.new(OpenSSL::PKey::RSA.new(2048), nil, use: 'sig')[:use]).to eq('sig') + expect(described_class.new(OpenSSL::PKey::RSA.new(2048), use: 'sig')[:use]).to eq('sig') end it 'converts string keys to symbol keys' do - expect(described_class.new(OpenSSL::PKey::RSA.new(2048), nil, { 'use' => 'sig' })[:use]).to eq('sig') + expect(described_class.new(OpenSSL::PKey::RSA.new(2048), { 'use' => 'sig' })[:use]).to eq('sig') end end end diff --git a/spec/jwk_spec.rb b/spec/jwk_spec.rb index 99a5093a..79ba8a2d 100644 --- a/spec/jwk_spec.rb +++ b/spec/jwk_spec.rb @@ -75,7 +75,7 @@ end context 'when a common parameter is given' do - subject { described_class.new(keypair, nil, params) } + subject { described_class.new(keypair, params) } let(:keypair) { rsa_key } let(:params) { { 'use' => 'sig' } } it 'sets the common parameter' do @@ -87,7 +87,7 @@ describe '.[]' do let(:params) { { use: 'sig' } } let(:keypair) { rsa_key } - subject { described_class.new(keypair, nil, params) } + subject { described_class.new(keypair, params) } it 'allows to read common parameters via the key-accessor' do expect(subject[:use]).to eq('sig') From 540ae3308e89141f78ef136ab85f1b35fcb503cd Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Wed, 12 Oct 2022 17:12:22 +0200 Subject: [PATCH 10/15] Update JWK examples in README.md --- README.md | 8 ++-- spec/integration/readme_examples_spec.rb | 58 +++++++++++++++++++++++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 07aa62bc..0d5ed74e 100644 --- a/README.md +++ b/README.md @@ -547,7 +547,7 @@ JWK is a JSON structure representing a cryptographic key. Currently only support If the kid is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`. The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases. ```ruby - jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'optional-kid') + jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid: 'optional-kid') payload = { data: 'data' } headers = { kid: jwk.kid } @@ -588,7 +588,9 @@ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks}) 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)) +# You can optionally add descriptive parameters to the JWK +desc_params = { kid: 'my-kid', use: 'sig' } +jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), desc_params) jwk_hash = jwk.export jwk_hash_with_private_key = jwk.export(include_private: true) @@ -603,7 +605,7 @@ JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint # OR JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint # OR -jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid_generator: ::JWT::JWK::Thumbprint) +jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK::Thumbprint) jwk_hash = jwk.export diff --git a/spec/integration/readme_examples_spec.rb b/spec/integration/readme_examples_spec.rb index 75c65e64..8da5ad1e 100644 --- a/spec/integration/readme_examples_spec.rb +++ b/spec/integration/readme_examples_spec.rb @@ -277,7 +277,7 @@ let(:logger_output) { StringIO.new } let(:logger) { Logger.new(logger_output) } - it 'works as expected' do + it 'works as expected (legacy)' do jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'optional-kid') payload = { data: 'data' } headers = { kid: jwk.kid } @@ -322,6 +322,52 @@ token = JWT.encode(payload, jwk.keypair, 'RS512', headers) expect { JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) }.to raise_error(JWT::DecodeError, 'Could not find public key for kid yet-another-new-kid') end + + it 'works as expected' do + jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid: 'optional-kid') + payload = { data: 'data' } + headers = { kid: jwk.kid } + + token = JWT.encode(payload, jwk.keypair, 'RS512', headers) + + # The jwk loader would fetch the set of JWKs from a trusted source, + # to avoid malicious invalidations some kind of protection needs to be implemented. + # This example only allows cache invalidations every 5 minutes. + jwk_loader = ->(options) do + if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300 + logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache") + @cached_keys = nil + end + @cached_keys ||= begin + @cache_last_update = Time.now.to_i + { keys: [jwk.export] } + end + end + + begin + JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) + rescue JWT::JWKError + # Handle problems with the provided JWKs + rescue JWT::DecodeError + # Handle other decode related issues e.g. no kid in header, no matching public key found etc. + end + + ## This is not in the example but verifies that the cache is invalidated after 5 minutes + jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'new-kid') + + headers = { kid: jwk.kid } + + token = JWT.encode(payload, jwk.keypair, 'RS512', headers) + @cache_last_update = Time.now.to_i - 301 + + JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) + expect(logger_output.string.chomp).to match(/^I, .* : Invalidating JWK cache. new-kid not found from previous cache/) + + jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'yet-another-new-kid') + headers = { kid: jwk.kid } + token = JWT.encode(payload, jwk.keypair, 'RS512', headers) + expect { JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) }.to raise_error(JWT::DecodeError, 'Could not find public key for kid yet-another-new-kid') + end end it 'JWK with thumbprint as kid via symbol' do @@ -344,12 +390,20 @@ expect(jwk_hash[:kid].size).to eq(43) end - it 'JWK with thumbprint given in the initializer' do + it 'JWK with thumbprint given in the initializer (legacy)' do jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid_generator: ::JWT::JWK::Thumbprint) jwk_hash = jwk.export expect(jwk_hash[:kid].size).to eq(43) end + + it 'JWK with thumbprint given in the initializer' do + jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), nil, kid_generator: ::JWT::JWK::Thumbprint) + + jwk_hash = jwk.export + + expect(jwk_hash[:kid].size).to eq(43) + end end end From 9d12031c21e16767a37ada8ef34599c7bae53057 Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Thu, 13 Oct 2022 11:00:19 +0200 Subject: [PATCH 11/15] JWK: Replace Hash#except for older Rubies --- lib/jwt/jwk/ec.rb | 2 +- lib/jwt/jwk/hmac.rb | 2 +- lib/jwt/jwk/rsa.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index 54feff87..f5272af0 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -47,7 +47,7 @@ def members def export(options = {}) exported = parameters.clone - exported = exported.except(*EC_PRIVATE_KEY_ELEMENTS) unless private? && options[:include_private] == true + exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true exported end diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index a969af48..a4a8884d 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -42,7 +42,7 @@ def public_key # See https://tools.ietf.org/html/rfc7517#appendix-A.3 def export(options = {}) exported = parameters.clone - exported = exported.except(*HMAC_PRIVATE_KEY_ELEMENTS) unless private? && options[:include_private] == true + exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true exported end diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index 8c9f4d1e..cd723b82 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -42,7 +42,7 @@ def public_key def export(options = {}) exported = parameters.clone - exported = exported.except(*RSA_PRIVATE_KEY_ELEMENTS) unless private? && options[:include_private] == true + exported.reject! { |k, _| RSA_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true exported end From 356e946c0c629463f7f359516c5a69961308aa35 Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Thu, 13 Oct 2022 11:14:13 +0200 Subject: [PATCH 12/15] JWK: Initialization optimizations --- lib/jwt/jwk.rb | 4 ++-- lib/jwt/jwk/ec.rb | 22 +++++++++++++--------- lib/jwt/jwk/hmac.rb | 21 ++++++++++++--------- lib/jwt/jwk/rsa.rb | 22 +++++++++++++--------- spec/jwk/rsa_spec.rb | 2 +- 5 files changed, 41 insertions(+), 30 deletions(-) diff --git a/lib/jwt/jwk.rb b/lib/jwt/jwk.rb index 10110468..329b1bde 100644 --- a/lib/jwt/jwk.rb +++ b/lib/jwt/jwk.rb @@ -14,10 +14,10 @@ def import(jwk_data) end.import(jwk_data) end - def create_from(keypair, params = nil, options = {}) + def create_from(key, params = nil, options = {}) mappings.fetch(keypair.class) do |klass| raise JWT::JWKError, "Cannot create JWK from a #{klass.name}" - end.new(keypair, params, options) + end.new(key, params, options) end def classes diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index f5272af0..c194d991 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -15,22 +15,26 @@ class EC < KeyBase # rubocop:disable Metrics/ClassLength EC_PRIVATE_KEY_ELEMENTS = %i[d].freeze EC_KEY_ELEMENTS = (EC_PRIVATE_KEY_ELEMENTS + EC_PUBLIC_KEY_ELEMENTS).freeze - def initialize(keypair, params = nil, options = {}) + def initialize(key, params = nil, options = {}) params ||= {} # For backwards compatibility when kid was a String params = { kid: params } if params.is_a?(String) - # Accept OpenSSL key as input - keypair = parse_ec_key(keypair) if keypair.is_a?(OpenSSL::PKey::EC) - - raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(Hash) + key_params = case key + when OpenSSL::PKey::EC # Accept OpenSSL key as input + @keypair = key # Preserve the object to avoid recreation + parse_ec_key(key) + when Hash + key.transform_keys(&:to_sym) + else + raise ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters' + end - keypair = keypair.transform_keys(&:to_sym) - params = params.transform_keys(&:to_sym) - check_jwk(keypair, params) + params = params.transform_keys(&:to_sym) + check_jwk(key_params, params) - super(options, keypair.merge(params)) + super(options, key_params.merge(params)) end def keypair diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index a4a8884d..1200bbef 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -9,22 +9,25 @@ class HMAC < KeyBase HMAC_PRIVATE_KEY_ELEMENTS = %i[k].freeze HMAC_KEY_ELEMENTS = (HMAC_PRIVATE_KEY_ELEMENTS + HMAC_PUBLIC_KEY_ELEMENTS).freeze - def initialize(keypair, params = nil, options = {}) + def initialize(key, params = nil, options = {}) params ||= {} # For backwards compatibility when kid was a String params = { kid: params } if params.is_a?(String) - # Accept String key as input - keypair = { kty: KTY, k: keypair } if keypair.is_a?(String) - - raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(Hash) + key_params = case key + when String # Accept String key as input + { kty: KTY, k: key } + when Hash + key.transform_keys(&:to_sym) + else + raise ArgumentError, 'key must be of type String or Hash with key parameters' + end - keypair = keypair.transform_keys(&:to_sym) - params = params.transform_keys(&:to_sym) - check_jwk(keypair, params) + params = params.transform_keys(&:to_sym) + check_jwk(key_params, params) - super(options, keypair.merge(params)) + super(options, key_params.merge(params)) end def keypair diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index cd723b82..d4f2fd5a 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -10,22 +10,26 @@ class RSA < KeyBase # rubocop:disable Metrics/ClassLength RSA_PRIVATE_KEY_ELEMENTS = %i[d p q dp dq qi].freeze RSA_KEY_ELEMENTS = (RSA_PRIVATE_KEY_ELEMENTS + RSA_PUBLIC_KEY_ELEMENTS).freeze - def initialize(keypair, params = nil, options = {}) + def initialize(key, params = nil, options = {}) params ||= {} # For backwards compatibility when kid was a String params = { kid: params } if params.is_a?(String) - # Accept OpenSSL key as input - keypair = parse_rsa_key(keypair) if keypair.is_a?(OpenSSL::PKey::RSA) - - raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(Hash) + key_params = case key + when OpenSSL::PKey::RSA # Accept OpenSSL key as input + @keypair = key # Preserve the object to avoid recreation + parse_rsa_key(key) + when Hash + key.transform_keys(&:to_sym) + else + raise ArgumentError, 'key must be of type OpenSSL::PKey::RSA or Hash with key parameters' + end - keypair = keypair.transform_keys(&:to_sym) - params = params.transform_keys(&:to_sym) - check_jwk(keypair, params) + params = params.transform_keys(&:to_sym) + check_jwk(key_params, params) - super(options, keypair.merge(params)) + super(options, key_params.merge(params)) end def keypair diff --git a/spec/jwk/rsa_spec.rb b/spec/jwk/rsa_spec.rb index 686a28f3..d677f54a 100644 --- a/spec/jwk/rsa_spec.rb +++ b/spec/jwk/rsa_spec.rb @@ -47,7 +47,7 @@ context 'when unsupported keypair is given' do let(:keypair) { 'key' } it 'raises an error' do - expect { subject }.to raise_error(ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA') + expect { subject }.to raise_error(ArgumentError) end end From b36428bc8ef803e3ecbaf86625132975274a1e3d Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Thu, 13 Oct 2022 15:38:43 +0200 Subject: [PATCH 13/15] JWK: More Documentation --- README.md | 14 +++++++++++++- lib/jwt/jwk.rb | 19 ++++++++++--------- spec/integration/readme_examples_spec.rb | 18 ++++++++++++++++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0d5ed74e..10ccacea 100644 --- a/README.md +++ b/README.md @@ -585,15 +585,27 @@ 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. +The ::JWT::JWK class can be used to import both JSON Web Keys and OpenSSL keys +and export to either format with and without the private key included. + +To include the private key in the export pass the `include_private` parameter to the export method. ```ruby +# Import a JWK Hash (showing an HMAC example) +jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' }) + +# Import an OpenSSL key # You can optionally add descriptive parameters to the JWK desc_params = { kid: 'my-kid', use: 'sig' } jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), desc_params) +# Export as JWK Hash (public key only by default) jwk_hash = jwk.export jwk_hash_with_private_key = jwk.export(include_private: true) + +# Export as OpenSSL key +public_key = jwk.public_key +private_key = jwk.keypair if jwk.private? ``` ### Key ID (kid) and JWKs diff --git a/lib/jwt/jwk.rb b/lib/jwt/jwk.rb index 329b1bde..578a59b3 100644 --- a/lib/jwt/jwk.rb +++ b/lib/jwt/jwk.rb @@ -5,17 +5,17 @@ module JWT module JWK class << self - def import(jwk_data) - jwk_kty = jwk_data[:kty] || jwk_data['kty'] - raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty + def create_from(key, params = nil, options = {}) + if key.is_a?(Hash) + jwk_kty = key[:kty] || key['kty'] + raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty - mappings.fetch(jwk_kty.to_s) do |kty| - raise JWT::JWKError, "Key type #{kty} not supported" - end.import(jwk_data) - end + return mappings.fetch(jwk_kty.to_s) do |kty| + raise JWT::JWKError, "Key type #{kty} not supported" + end.new(key, params, options) + end - def create_from(key, params = nil, options = {}) - mappings.fetch(keypair.class) do |klass| + mappings.fetch(key.class) do |klass| raise JWT::JWKError, "Cannot create JWK from a #{klass.name}" end.new(key, params, options) end @@ -26,6 +26,7 @@ def classes end alias new create_from + alias import create_from private diff --git a/spec/integration/readme_examples_spec.rb b/spec/integration/readme_examples_spec.rb index 8da5ad1e..9e42d502 100644 --- a/spec/integration/readme_examples_spec.rb +++ b/spec/integration/readme_examples_spec.rb @@ -370,6 +370,24 @@ end end + it 'JWK import and export' do + # Import a JWK Hash (showing an HMAC example) + _jwk = JWT::JWK.new({ kty: 'oct', k: 'my-secret', kid: 'my-kid' }) + + # Import an OpenSSL key + # You can optionally add descriptive parameters to the JWK + desc_params = { kid: 'my-kid', use: 'sig' } + jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), desc_params) + + # Export as JWK Hash (public key only by default) + _jwk_hash = jwk.export + _jwk_hash_with_private_key = jwk.export(include_private: true) + + # Export as OpenSSL key + _public_key = jwk.public_key + _private_key = jwk.keypair if jwk.private? + end + it 'JWK with thumbprint as kid via symbol' do JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint From c4c4496703c2a41c418662aed8091256f00d0cdd Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Fri, 14 Oct 2022 10:38:45 +0200 Subject: [PATCH 14/15] Remove superfluous Rubocop disable --- lib/jwt/jwk/rsa.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index d4f2fd5a..d99b08f0 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -2,7 +2,7 @@ module JWT module JWK - class RSA < KeyBase # rubocop:disable Metrics/ClassLength + class RSA < KeyBase BINARY = 2 KTY = 'RSA' KTYS = [KTY, OpenSSL::PKey::RSA].freeze From 487f1155a2dd6e3f3003d8ca2bc14d05079717a3 Mon Sep 17 00:00:00 2001 From: Thomas Bellebaum Date: Fri, 14 Oct 2022 11:07:57 +0200 Subject: [PATCH 15/15] JWK: Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bd19790..f20db635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Features:** - Support custom algorithms by passing algorithm objects[#512](https://github.com/jwt/ruby-jwt/pull/512) ([@anakinj](https://github.com/anakinj)). +- Support descriptive (not key related) JWK parameters[#520](https://github.com/jwt/ruby-jwt/pull/520) ([@bellebaum](https://github.com/bellebaum)). - Your contribution here **Fixes and enhancements:**