From 84599ac51b650b7c25be18c6a34efb176fa2c033 Mon Sep 17 00:00:00 2001 From: Egon Zemmer Date: Thu, 24 Sep 2020 23:50:06 +0200 Subject: [PATCH 1/8] Add support for JWKs with HMAC key type. --- lib/jwt/jwk.rb | 5 +++- lib/jwt/jwk/hmac.rb | 52 +++++++++++++++++++++++++++++++++++++++ spec/jwk/hmac_spec.rb | 57 +++++++++++++++++++++++++++++++++++++++++++ spec/jwk_spec.rb | 8 +++--- 4 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 lib/jwt/jwk/hmac.rb create mode 100644 spec/jwk/hmac_spec.rb diff --git a/lib/jwt/jwk.rb b/lib/jwt/jwk.rb index 7bef7430..e40bb4f0 100644 --- a/lib/jwt/jwk.rb +++ b/lib/jwt/jwk.rb @@ -1,13 +1,16 @@ # frozen_string_literal: true require_relative 'jwk/rsa' +require_relative 'jwk/hmac' require_relative 'jwk/key_finder' module JWT module JWK MAPPINGS = { 'RSA' => ::JWT::JWK::RSA, - OpenSSL::PKey::RSA => ::JWT::JWK::RSA + OpenSSL::PKey::RSA => ::JWT::JWK::RSA, + 'oct' => ::JWT::JWK::HMAC, + String => ::JWT::JWK::HMAC }.freeze class << self diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb new file mode 100644 index 00000000..aead748a --- /dev/null +++ b/lib/jwt/jwk/hmac.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module JWT + module JWK + class HMAC + attr_reader :key + attr_reader :kid + + KTY = 'oct'.freeze + + def initialize(key, kid = nil) + raise ArgumentError, 'key must be of type String' unless key.is_a?(String) + + @key = key + @kid = kid || generate_kid(@key) + end + + def private? + true + end + + # See https://tools.ietf.org/html/rfc7517#appendix-A.3 + def export + { + kty: KTY, + k: key, + kid: kid + } + end + + private + + def generate_kid(hmac_key) + sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(hmac_key), + OpenSSL::ASN1::UTF8String.new(KTY)]) + OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) + end + + class << self + + def import(jwk_data) + jwk_k = jwk_data[:k] || jwk_data['k'] + jwk_kid = jwk_data[:kid] || jwk_data['kid'] + + raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k + + self.new(jwk_k, jwk_kid) + end + end + end + end +end diff --git a/spec/jwk/hmac_spec.rb b/spec/jwk/hmac_spec.rb new file mode 100644 index 00000000..9fbe21e3 --- /dev/null +++ b/spec/jwk/hmac_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' +require 'jwt' + +describe JWT::JWK::HMAC do + let(:hmac_key) { 'secret-key' } + + describe '.new' do + subject { described_class.new(key) } + + context 'when a secret key given' do + let(:key) { hmac_key } + it 'creates an instance of the class' do + expect(subject).to be_a described_class + expect(subject.private?).to eq true + end + end + end + + describe '#export' do + let(:kid) { nil } + subject { described_class.new(key, kid).export } + + context 'when key is exported' do + let(:key) { hmac_key } + it 'returns a hash with the key' do + expect(subject).to be_a Hash + expect(subject).to include(:kty, :kid, :k) + end + end + end + + describe '.import' do + subject { described_class.import(params) } + let(:exported_key) { described_class.new(key).export } + + context 'when secret key is given' do + let(:key) { hmac_key } + let(:params) { exported_key } + + it 'returns a key' do + expect(subject).to be_a described_class + expect(subject.export).to eq(exported_key) + end + + context 'with a custom "kid" value' do + let(:exported_key) { + super().merge(kid: 'custom_key_identifier') + } + it 'imports that "kid" value' do + expect(subject.kid).to eq('custom_key_identifier') + end + end + end + end +end diff --git a/spec/jwk_spec.rb b/spec/jwk_spec.rb index 3ab5374f..12d85f89 100644 --- a/spec/jwk_spec.rb +++ b/spec/jwk_spec.rb @@ -43,11 +43,9 @@ it { is_expected.to be_a ::JWT::JWK::RSA } end - context 'when unsupported key is given' do - let(:keypair) { 'key' } - it 'raises an error' do - expect { subject }.to raise_error(::JWT::JWKError, 'Cannot create JWK from a String') - end + context 'when secret key is given' do + let(:keypair) { 'secret-key' } + it { is_expected.to be_a ::JWT::JWK::HMAC } end end end From f540c500fe72ed7ed6a9608f50bb82e7c934b56e Mon Sep 17 00:00:00 2001 From: Egon Zemmer Date: Fri, 25 Sep 2020 00:01:48 +0200 Subject: [PATCH 2/8] Fix code climate issue. --- lib/jwt/jwk/hmac.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index aead748a..fd9b41be 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -37,7 +37,6 @@ def generate_kid(hmac_key) end class << self - def import(jwk_data) jwk_k = jwk_data[:k] || jwk_data['k'] jwk_kid = jwk_data[:kid] || jwk_data['kid'] From f91e8c0ff6b32bddc842e108cd8557023f65b2a5 Mon Sep 17 00:00:00 2001 From: Egon Zemmer Date: Fri, 25 Sep 2020 00:58:34 +0200 Subject: [PATCH 3/8] Use the same attribute reader name "keypair" like other jwk's. --- lib/jwt/jwk/hmac.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index fd9b41be..65d3e22b 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -3,16 +3,16 @@ module JWT module JWK class HMAC - attr_reader :key + attr_reader :keypair attr_reader :kid KTY = 'oct'.freeze - def initialize(key, kid = nil) - raise ArgumentError, 'key must be of type String' unless key.is_a?(String) + def initialize(keypair, kid = nil) + raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(String) - @key = key - @kid = kid || generate_kid(@key) + @keypair = keypair + @kid = kid || generate_kid(@keypair) end def private? @@ -23,7 +23,7 @@ def private? def export { kty: KTY, - k: key, + k: keypair, kid: kid } end From 55c4c6365c72759969fa84ad45f9fdfe5fd64d0b Mon Sep 17 00:00:00 2001 From: Egon Zemmer Date: Fri, 25 Sep 2020 10:51:45 +0200 Subject: [PATCH 4/8] Add export options to be able to export secret key. --- lib/jwt/jwk/hmac.rb | 15 ++++++++++++--- spec/jwk/hmac_spec.rb | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index 65d3e22b..96256973 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -19,13 +19,22 @@ def private? true end + def public_key + nil + end + # See https://tools.ietf.org/html/rfc7517#appendix-A.3 - def export - { + def export(options = {}) + ret = { kty: KTY, - k: keypair, kid: kid } + + return ret unless private? && options[:include_private] == true + + ret.merge( + k: keypair + ) end private diff --git a/spec/jwk/hmac_spec.rb b/spec/jwk/hmac_spec.rb index 9fbe21e3..31ad4ee0 100644 --- a/spec/jwk/hmac_spec.rb +++ b/spec/jwk/hmac_spec.rb @@ -20,10 +20,19 @@ describe '#export' do let(:kid) { nil } - subject { described_class.new(key, kid).export } context 'when key is exported' do let(:key) { hmac_key } + subject { described_class.new(key, kid).export } + it 'returns a hash with the key' do + expect(subject).to be_a Hash + expect(subject).to include(:kty, :kid) + end + end + + context 'when key is exported with private key' do + let(:key) { hmac_key } + subject { described_class.new(key, kid).export(include_private: true) } it 'returns a hash with the key' do expect(subject).to be_a Hash expect(subject).to include(:kty, :kid, :k) @@ -33,7 +42,7 @@ describe '.import' do subject { described_class.import(params) } - let(:exported_key) { described_class.new(key).export } + let(:exported_key) { described_class.new(key).export(include_private: true) } context 'when secret key is given' do let(:key) { hmac_key } @@ -41,7 +50,7 @@ it 'returns a key' do expect(subject).to be_a described_class - expect(subject.export).to eq(exported_key) + expect(subject.export(include_private: true)).to eq(exported_key) end context 'with a custom "kid" value' do From 90d49d6408d88902678c0972d37dafb2fb2cb86a Mon Sep 17 00:00:00 2001 From: Egon Zemmer Date: Fri, 25 Sep 2020 11:47:51 +0200 Subject: [PATCH 5/8] Add JWK factory. --- lib/jwt/jwk.rb | 1 + lib/jwt/jwk/factory.rb | 33 +++++++++++++++++++++++++++++++++ lib/jwt/jwk/hmac.rb | 7 ++----- 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 lib/jwt/jwk/factory.rb diff --git a/lib/jwt/jwk.rb b/lib/jwt/jwk.rb index e40bb4f0..f6e452e8 100644 --- a/lib/jwt/jwk.rb +++ b/lib/jwt/jwk.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative 'jwk/factory' require_relative 'jwk/rsa' require_relative 'jwk/hmac' require_relative 'jwk/key_finder' diff --git a/lib/jwt/jwk/factory.rb b/lib/jwt/jwk/factory.rb new file mode 100644 index 00000000..3771e792 --- /dev/null +++ b/lib/jwt/jwk/factory.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module JWT + module JWK + class Factory + attr_reader :keypair + attr_reader :kid + + def initialize(keypair, kid = nil) + @keypair = keypair + @kid = kid + end + + def private? + raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + + def public_key + raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + + def export(options = {}) + raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + + class << self + def import(jwk_data) + raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" + end + end + end + end +end diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index 96256973..b398626b 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -2,16 +2,13 @@ module JWT module JWK - class HMAC - attr_reader :keypair - attr_reader :kid - + class HMAC < Factory KTY = 'oct'.freeze def initialize(keypair, kid = nil) raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(String) - @keypair = keypair + super @kid = kid || generate_kid(@keypair) end From 3b62f2b6887e8ca531cccf6771c3deb5672dab73 Mon Sep 17 00:00:00 2001 From: Egon Zemmer Date: Fri, 25 Sep 2020 11:58:09 +0200 Subject: [PATCH 6/8] Solve unused variable. --- lib/jwt/jwk/factory.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jwt/jwk/factory.rb b/lib/jwt/jwk/factory.rb index 3771e792..17545255 100644 --- a/lib/jwt/jwk/factory.rb +++ b/lib/jwt/jwk/factory.rb @@ -19,12 +19,12 @@ def public_key raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end - def export(options = {}) + def export(_options = {}) raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end class << self - def import(jwk_data) + def import(_jwk_data) raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'" end end From 37640f0a370fb838de6cadf7197a0777c83c7ba4 Mon Sep 17 00:00:00 2001 From: Egon Zemmer Date: Fri, 25 Sep 2020 12:00:20 +0200 Subject: [PATCH 7/8] Attr reader to one line for both attributes. --- lib/jwt/jwk/factory.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/jwt/jwk/factory.rb b/lib/jwt/jwk/factory.rb index 17545255..2e615d73 100644 --- a/lib/jwt/jwk/factory.rb +++ b/lib/jwt/jwk/factory.rb @@ -3,8 +3,7 @@ module JWT module JWK class Factory - attr_reader :keypair - attr_reader :kid + attr_reader :keypair, :kid def initialize(keypair, kid = nil) @keypair = keypair From f4fdaadfa659217739503a6b9f8866b64f4a2aa6 Mon Sep 17 00:00:00 2001 From: Egon Zemmer Date: Fri, 25 Sep 2020 16:50:24 +0200 Subject: [PATCH 8/8] Change class name Factory to KeyAbstract. --- lib/jwt/jwk.rb | 2 +- lib/jwt/jwk/hmac.rb | 2 +- lib/jwt/jwk/{factory.rb => key_abstract.rb} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename lib/jwt/jwk/{factory.rb => key_abstract.rb} (97%) diff --git a/lib/jwt/jwk.rb b/lib/jwt/jwk.rb index f6e452e8..04aee444 100644 --- a/lib/jwt/jwk.rb +++ b/lib/jwt/jwk.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'jwk/factory' +require_relative 'jwk/key_abstract' require_relative 'jwk/rsa' require_relative 'jwk/hmac' require_relative 'jwk/key_finder' diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index b398626b..8ba66e1a 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -2,7 +2,7 @@ module JWT module JWK - class HMAC < Factory + class HMAC < KeyAbstract KTY = 'oct'.freeze def initialize(keypair, kid = nil) diff --git a/lib/jwt/jwk/factory.rb b/lib/jwt/jwk/key_abstract.rb similarity index 97% rename from lib/jwt/jwk/factory.rb rename to lib/jwt/jwk/key_abstract.rb index 2e615d73..b9271469 100644 --- a/lib/jwt/jwk/factory.rb +++ b/lib/jwt/jwk/key_abstract.rb @@ -2,7 +2,7 @@ module JWT module JWK - class Factory + class KeyAbstract attr_reader :keypair, :kid def initialize(keypair, kid = nil)