From d6ee141bcf325a6c0941c209ff4bd7df6558aa24 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Wed, 28 Dec 2022 20:38:39 +0200 Subject: [PATCH] JWK OKP Ed25519 support using rbnacl - Prepare to deprecate the ::JWT::JWK::[RSA/EC/HMAC]#keypair method in favor of methods describing the use --- CHANGELOG.md | 1 + README.md | 8 +- lib/jwt/jwk.rb | 1 + lib/jwt/jwk/ec.rb | 35 +++++-- lib/jwt/jwk/hmac.rb | 16 ++- lib/jwt/jwk/key_finder.rb | 2 +- lib/jwt/jwk/okp_rbnacl.rb | 110 +++++++++++++++++++ lib/jwt/jwk/rsa.rb | 28 +++-- spec/integration/readme_examples_spec.rb | 14 +-- spec/jwk/decode_with_jwk_spec.rb | 50 ++++++--- spec/jwk/ec_spec.rb | 8 ++ spec/jwk/hmac_spec.rb | 23 +++- spec/jwk/okp_rbnacl_spec.rb | 128 +++++++++++++++++++++++ spec/jwk/rsa_spec.rb | 8 ++ 14 files changed, 377 insertions(+), 55 deletions(-) create mode 100644 lib/jwt/jwk/okp_rbnacl.rb create mode 100644 spec/jwk/okp_rbnacl_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 0224d800..77325d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ **Features:** +- Support OKP (Ed25519) keys for JWKs [#540](https://github.com/jwt/ruby-jwt/pull/540) ([@anakinj](https://github.com/anakinj)). - Your contribution here - JWK Sets can now be used for tokens with nil kid[#543](https://github.com/jwt/ruby-jwt/pull/543) ([@bellebaum](https://github.com/bellebaum)) diff --git a/README.md b/README.md index 8e6a961a..4ebf0e73 100644 --- a/README.md +++ b/README.md @@ -569,7 +569,7 @@ end ### JSON Web Key (JWK) -JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC and HMAC keys. +JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC, OKP and HMAC keys. OKP support requires [RbNaCl](https://github.com/RubyCrypto/rbnacl) and currently only supports the Ed25519 curve. To encode a JWT using your JWK: @@ -579,7 +579,7 @@ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters) # Encoding payload = { data: 'data' } -token = JWT.encode(payload, jwk.keypair, jwk[:alg], kid: jwk[:kid]) +token = JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid]) # JSON Web Key Set for advertising your signing keys jwks_hash = JWT::JWK::Set.new(jwk).export @@ -653,8 +653,8 @@ 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? +public_key = jwk.verify_key +private_key = jwk.signing_key if jwk.private? # You can also import and export entire JSON Web Key Sets jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] } diff --git a/lib/jwt/jwk.rb b/lib/jwt/jwk.rb index f934641e..d8d14ae6 100644 --- a/lib/jwt/jwk.rb +++ b/lib/jwt/jwk.rb @@ -52,3 +52,4 @@ def generate_mappings require_relative 'jwk/ec' require_relative 'jwk/rsa' require_relative 'jwk/hmac' +require_relative 'jwk/okp_rbnacl' if ::JWT.rbnacl? diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb index a19008d7..cb545f89 100644 --- a/lib/jwt/jwk/ec.rb +++ b/lib/jwt/jwk/ec.rb @@ -5,9 +5,6 @@ module JWT module JWK class EC < KeyBase # rubocop:disable Metrics/ClassLength - extend Forwardable - def_delegators :keypair, :public_key - KTY = 'EC' KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze BINARY = 2 @@ -24,17 +21,29 @@ def initialize(key, params = nil, options = {}) key_params = extract_key_params(key) params = params.transform_keys(&:to_sym) - check_jwk(key_params, params) + check_jwk_params!(key_params, params) super(options, key_params.merge(params)) end def keypair - @keypair ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d]) + ec_key end def private? - keypair.private_key? + ec_key.private_key? + end + + def signing_key + ec_key + end + + def verify_key + ec_key + end + + def public_key + ec_key end def members @@ -48,7 +57,7 @@ def export(options = {}) end def key_digest - _crv, x_octets, y_octets = keypair_components(keypair) + _crv, x_octets, y_octets = keypair_components(ec_key) sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)), OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))]) OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) @@ -64,12 +73,16 @@ def []=(key, value) private + def ec_key + @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d]) + end + def extract_key_params(key) case key when JWT::JWK::EC key.export(include_private: true) when OpenSSL::PKey::EC # Accept OpenSSL key as input - @keypair = key # Preserve the object to avoid recreation + @ec_key = key # Preserve the object to avoid recreation parse_ec_key(key) when Hash key.transform_keys(&:to_sym) @@ -78,10 +91,10 @@ def extract_key_params(key) end end - def check_jwk(keypair, params) + def check_jwk_params!(key_params, 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] + raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY + raise JWT::JWKError, 'Key format is invalid for EC' unless key_params[:crv] && key_params[:x] && key_params[:y] end def keypair_components(ec_keypair) diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index c3245ba5..be371471 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -24,7 +24,7 @@ def initialize(key, params = nil, options = {}) end def keypair - self[:k] + secret end def private? @@ -35,6 +35,14 @@ def public_key nil end + def verify_key + secret + end + + def signing_key + secret + end + # See https://tools.ietf.org/html/rfc7517#appendix-A.3 def export(options = {}) exported = parameters.clone @@ -46,8 +54,6 @@ def members HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] } end - alias signing_key keypair # for backwards compatibility - def key_digest sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key), OpenSSL::ASN1::UTF8String.new(KTY)]) @@ -64,6 +70,10 @@ def []=(key, value) private + def secret + self[:k] + end + def extract_key_params(key) case key when JWT::JWK::HMAC diff --git a/lib/jwt/jwk/key_finder.rb b/lib/jwt/jwk/key_finder.rb index 68e1981e..b3ccdc0c 100644 --- a/lib/jwt/jwk/key_finder.rb +++ b/lib/jwt/jwk/key_finder.rb @@ -23,7 +23,7 @@ def key_for(kid) raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any? raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk - jwk.keypair + jwk.verify_key end private diff --git a/lib/jwt/jwk/okp_rbnacl.rb b/lib/jwt/jwk/okp_rbnacl.rb new file mode 100644 index 00000000..cb24211f --- /dev/null +++ b/lib/jwt/jwk/okp_rbnacl.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +module JWT + module JWK + class OKPRbNaCl < KeyBase + KTY = 'OKP' + KTYS = [KTY, JWT::JWK::OKPRbNaCl, RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey].freeze + OKP_PUBLIC_KEY_ELEMENTS = %i[kty n x].freeze + OKP_PRIVATE_KEY_ELEMENTS = %i[d].freeze + + def initialize(key, params = nil, options = {}) + params ||= {} + + # For backwards compatibility when kid was a String + params = { kid: params } if params.is_a?(String) + + key_params = extract_key_params(key) + + params = params.transform_keys(&:to_sym) + check_jwk_params!(key_params, params) + super(options, key_params.merge(params)) + end + + def verify_key + return @verify_key if defined?(@verify_key) + + @verify_key = verify_key_from_parameters + end + + def signing_key + return @signing_key if defined?(@signing_key) + + @signing_key = signing_key_from_parameters + end + + def key_digest + Thumbprint.new(self).to_s + end + + def private? + !signing_key.nil? + end + + def members + OKP_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] } + end + + def export(options = {}) + exported = parameters.clone + exported.reject! { |k, _| OKP_PRIVATE_KEY_ELEMENTS.include?(k) } unless private? && options[:include_private] == true + exported + end + + private + + def extract_key_params(key) + case key + when JWT::JWK::KeyBase + key.export(include_private: true) + when RbNaCl::Signatures::Ed25519::SigningKey + @signing_key = key + @verify_key = key.verify_key + parse_okp_key_params(@verify_key, @signing_key) + when RbNaCl::Signatures::Ed25519::VerifyKey + @signing_key = nil + @verify_key = key + parse_okp_key_params(@verify_key) + when Hash + key.transform_keys(&:to_sym) + else + raise ArgumentError, 'key must be of type RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey or Hash with key parameters' + end + end + + def check_jwk_params!(key_params, _given_params) + raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY + end + + def parse_okp_key_params(verify_key, signing_key = nil) + params = { + kty: KTY, + crv: 'Ed25519', + x: ::JWT::Base64.url_encode(verify_key.to_bytes) + } + + if signing_key + params[:d] = ::JWT::Base64.url_encode(signing_key.to_bytes) + end + + params + end + + def verify_key_from_parameters + RbNaCl::Signatures::Ed25519::VerifyKey.new(::JWT::Base64.url_decode(self[:x])) + end + + def signing_key_from_parameters + return nil unless self[:d] + + RbNaCl::Signatures::Ed25519::SigningKey.new(::JWT::Base64.url_decode(self[:d])) + end + + class << self + def import(jwk_data) + new(jwk_data) + end + end + end + end +end diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index 9c8cfc86..6dfbe844 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -22,21 +22,29 @@ def initialize(key, params = nil, options = {}) key_params = extract_key_params(key) params = params.transform_keys(&:to_sym) - check_jwk(key_params, params) + check_jwk_params!(key_params, params) super(options, key_params.merge(params)) end def keypair - @keypair ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty]))) + rsa_key end def private? - keypair.private? + rsa_key.private? end def public_key - keypair.public_key + rsa_key.public_key + end + + def signing_key + rsa_key if private? + end + + def verify_key + rsa_key.public_key end def export(options = {}) @@ -65,12 +73,16 @@ def []=(key, value) private + def rsa_key + @rsa_key ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty]))) + end + def extract_key_params(key) case key when JWT::JWK::RSA key.export(include_private: true) when OpenSSL::PKey::RSA # Accept OpenSSL key as input - @keypair = key # Preserve the object to avoid recreation + @rsa_key = key # Preserve the object to avoid recreation parse_rsa_key(key) when Hash key.transform_keys(&:to_sym) @@ -79,10 +91,10 @@ def extract_key_params(key) end end - def check_jwk(keypair, params) + def check_jwk_params!(key_params, 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] + raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY + raise JWT::JWKError, 'Key format is invalid for RSA' unless key_params[:n] && key_params[:e] end def parse_rsa_key(key) diff --git a/spec/integration/readme_examples_spec.rb b/spec/integration/readme_examples_spec.rb index 11930b08..8e917f33 100644 --- a/spec/integration/readme_examples_spec.rb +++ b/spec/integration/readme_examples_spec.rb @@ -281,7 +281,7 @@ # Encoding payload = { data: 'data' } - token = JWT.encode(payload, jwk.keypair, jwk[:alg], kid: jwk[:kid]) + token = JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid]) # JSON Web Key Set for advertising your signing keys jwks_hash = JWT::JWK::Set.new(jwk).export @@ -303,7 +303,7 @@ payload = { data: 'data' } headers = { kid: jwk.kid } - token = JWT.encode(payload, jwk.keypair, 'RS512', headers) + token = JWT.encode(payload, jwk.signing_key, '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. @@ -332,7 +332,7 @@ headers = { kid: jwk.kid } - token = JWT.encode(payload, jwk.keypair, 'RS512', headers) + token = JWT.encode(payload, jwk.signing_key, 'RS512', headers) @cache_last_update = Time.now.to_i - 301 JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) @@ -340,7 +340,7 @@ 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) + token = JWT.encode(payload, jwk.signing_key, '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 @@ -350,7 +350,7 @@ payload = { data: 'data' } headers = { kid: jwk.kid } - token = JWT.encode(payload, jwk.keypair, 'RS512', headers) + token = JWT.encode(payload, jwk.signing_key, 'RS512', headers) jwks_loader = ->(options) do # The jwk loader would fetch the set of JWKs from a trusted source. @@ -394,8 +394,8 @@ _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? + _public_key = jwk.verify_key + _private_key = jwk.signing_key if jwk.private? # You can also import and export entire JSON Web Key Sets jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] } diff --git a/spec/jwk/decode_with_jwk_spec.rb b/spec/jwk/decode_with_jwk_spec.rb index 66d125c1..ac2f0f18 100644 --- a/spec/jwk/decode_with_jwk_spec.rb +++ b/spec/jwk/decode_with_jwk_spec.rb @@ -7,12 +7,13 @@ let(:public_jwks) { { keys: [jwk.export, { kid: 'not_the_correct_one', kty: 'oct', k: 'secret' }] } } let(:token_payload) { { 'data' => 'something' } } let(:token_headers) { { kid: jwk.kid } } - let(:signed_token) { described_class.encode(token_payload, jwk.keypair, 'RS512', token_headers) } + let(:algorithm) { 'RS512' } + let(:signed_token) { described_class.encode(token_payload, jwk.signing_key, algorithm, token_headers) } context 'when JWK features are used manually' do it 'is able to decode the token' do - payload, _header = described_class.decode(signed_token, nil, true, { algorithms: ['RS512'] }) do |header, _payload| - JWT::JWK.import(public_jwks[:keys].find { |key| key[:kid] == header['kid'] }).keypair + payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm] }) do |header, _payload| + JWT::JWK.import(public_jwks[:keys].find { |key| key[:kid] == header['kid'] }).verify_key end expect(payload).to eq(token_payload) end @@ -21,7 +22,7 @@ context 'when jwk keys are given as an array' do context 'and kid is in the set' do it 'is able to decode the token' do - payload, _header = described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: public_jwks }) + payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) expect(payload).to eq(token_payload) end end @@ -31,7 +32,7 @@ public_jwks[:keys].first[:kid] = 'NOT_A_MATCH' end it 'raises an exception' do - expect { described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: public_jwks }) }.to raise_error( + expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( JWT::DecodeError, /Could not find public key for kid .*/ ) end @@ -40,7 +41,7 @@ context 'no keys are found in the set' do let(:public_jwks) { { keys: [] } } it 'raises an exception' do - expect { described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: public_jwks }) }.to raise_error( + expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( JWT::DecodeError, /No keys found in jwks/ ) end @@ -49,7 +50,7 @@ context 'token does not know the kid' do let(:token_headers) { {} } it 'raises an exception' do - expect { described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: public_jwks }) }.to raise_error( + expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error( JWT::DecodeError, 'No key id (kid) found from token headers' ) end @@ -58,7 +59,7 @@ context 'when jwk keys are loaded using a proc/lambda' do it 'decodes the token' do - payload, _header = described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: lambda { |_opts| public_jwks } }) + payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: lambda { |_opts| public_jwks } }) expect(payload).to eq(token_payload) end end @@ -66,7 +67,7 @@ context 'when jwk keys are rotated' do it 'decodes the token' do key_loader = ->(options) { options[:invalidate] ? public_jwks : { keys: [] } } - payload, _header = described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: key_loader }) + payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: key_loader }) expect(payload).to eq(token_payload) end end @@ -74,7 +75,7 @@ context 'when jwk keys are loaded from JSON with string keys' do it 'decodes the token' do key_loader = ->(_options) { JSON.parse(JSON.generate(public_jwks)) } - payload, _header = described_class.decode(signed_token, nil, true, { algorithms: ['RS512'], jwks: key_loader }) + payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: key_loader }) expect(payload).to eq(token_payload) end end @@ -123,27 +124,27 @@ end context 'when EC key is pointed to as RSA public key' do - let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, rsa_jwk.keypair, 'RS512', { kid: ec_jwk_secp384r1.kid }) } + let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, rsa_jwk.signing_key, algorithm, { kid: ec_jwk_secp384r1.kid }) } it 'fails in some way' do - expect { described_class.decode(signed_token, nil, true, algorithms: ['RS512'], jwks: jwks) }.to( + expect { described_class.decode(signed_token, nil, true, algorithms: [algorithm], jwks: jwks) }.to( raise_error(JWT::VerificationError, 'Signature verification raised') ) end end context 'when HMAC secret is pointed to as RSA public key' do - let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, rsa_jwk.keypair, 'RS512', { kid: hmac_jwk.kid }) } + let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, rsa_jwk.signing_key, algorithm, { kid: hmac_jwk.kid }) } it 'fails in some way' do - expect { described_class.decode(signed_token, nil, true, algorithms: ['RS512'], jwks: jwks) }.to( + expect { described_class.decode(signed_token, nil, true, algorithms: [algorithm], jwks: jwks) }.to( raise_error(NoMethodError, /undefined method `verify' for "secret":String/) ) end end context 'when HMAC secret is pointed to as EC public key' do - let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.keypair, 'ES384', { kid: hmac_jwk.kid }) } + let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.signing_key, 'ES384', { kid: hmac_jwk.kid }) } it 'fails in some way' do expect { described_class.decode(signed_token, nil, true, algorithms: ['ES384'], jwks: jwks) }.to( @@ -153,7 +154,7 @@ end context 'when ES384 key is pointed to as ES512 key' do - let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.keypair, 'ES512', { kid: ec_jwk_secp521r1.kid }) } + let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.signing_key, 'ES512', { kid: ec_jwk_secp521r1.kid }) } it 'fails in some way' do expect { described_class.decode(signed_token, nil, true, algorithms: ['ES512'], jwks: jwks) }.to( @@ -161,6 +162,23 @@ ) end end + + if defined?(RbNaCl) + context 'when OKP keys are used' do + before do + skip('Requires the rbnacl gem') unless ::JWT.rbnacl? + end + + let(:keypair) { RbNaCl::Signatures::Ed25519::SigningKey.new(SecureRandom.hex) } + let(:algorithm) { 'ED25519' } + + it 'decodes the token' do + key_loader = ->(_options) { JSON.parse(JSON.generate(public_jwks)) } + payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: key_loader }) + expect(payload).to eq(token_payload) + end + end + end end end end diff --git a/spec/jwk/ec_spec.rb b/spec/jwk/ec_spec.rb index 2ff5f01b..ba67c46e 100644 --- a/spec/jwk/ec_spec.rb +++ b/spec/jwk/ec_spec.rb @@ -23,6 +23,14 @@ end end + describe '#keypair' do + subject(:jwk) { described_class.new(ec_key) } + + it 'warns to stderr' do + expect(jwk.keypair).to eq(ec_key) + end + end + describe '#export' do let(:kid) { nil } subject { described_class.new(keypair, kid).export } diff --git a/spec/jwk/hmac_spec.rb b/spec/jwk/hmac_spec.rb index ae2c3f27..dcc7d4ce 100644 --- a/spec/jwk/hmac_spec.rb +++ b/spec/jwk/hmac_spec.rb @@ -2,19 +2,24 @@ RSpec.describe JWT::JWK::HMAC do let(:hmac_key) { 'secret-key' } + let(:key) { hmac_key } + subject(:jwk) { described_class.new(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 + expect(jwk).to be_a described_class + expect(jwk.private?).to eq true end end end + describe '#keypair' do + it 'returns a string' do + expect(jwk.keypair).to eq(key) + end + end + describe '#export' do let(:kid) { nil } @@ -69,4 +74,12 @@ end end end + + describe '#[]=' do + context 'when k is given' do + it 'raises an error' do + expect { jwk[:k] = 'new_secret' }.to raise_error(ArgumentError, 'cannot overwrite cryptographic key attributes') + end + end + end end diff --git a/spec/jwk/okp_rbnacl_spec.rb b/spec/jwk/okp_rbnacl_spec.rb new file mode 100644 index 00000000..08bd9ef7 --- /dev/null +++ b/spec/jwk/okp_rbnacl_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'securerandom' + +describe 'JWT::JWK::OKPRbNaCl' do + let(:described_class) { JWT::JWK::OKPRbNaCl } + let(:private_key) { RbNaCl::Signatures::Ed25519::SigningKey.new(SecureRandom.hex) } + let(:public_key) { private_key.verify_key } + let(:key) { nil } + + subject(:instance) { described_class.new(key) } + + before do + skip('Requires the rbnacl gem') unless ::JWT.rbnacl? + end + + describe '.new' do + context 'when private key is given' do + let(:key) { private_key } + it { is_expected.to be_a(described_class) } + end + context 'when public key is given' do + let(:key) { public_key } + it { is_expected.to be_a(described_class) } + end + context 'when something else than a public or private key is given' do + let(:key) { OpenSSL::PKey::RSA.new(2048) } + it 'raises an ArgumentError' do + expect { instance }.to raise_error(ArgumentError) + end + end + + context 'when jwk parameters given' do + let(:key) do + { + kty: 'OKP', + use: 'sig', + crv: 'Ed25519', + kid: '27zV', + x: '0I6olrZGYml7JGusuKJW9G7D0DZ9UormSady9kR7V4Q' + } + end + it { is_expected.to be_a(described_class) } + end + end + + describe '#verify_key' do + let(:key) { private_key } + subject { instance.verify_key } + it 'is the verify key' do + expect(subject).to be_a(RbNaCl::Signatures::Ed25519::VerifyKey) + end + end + + describe '#private?' do + subject { instance.private? } + context 'when private key is given' do + let(:key) { private_key } + it { is_expected.to eq(true) } + end + context 'when public key is given' do + let(:key) { public_key } + it { is_expected.to eq(false) } + end + end + + describe '#export' do + let(:options) { {} } + subject { instance.export(options) } + context 'when private key is given' do + let(:key) { private_key } + it 'exports the public key' do + expect(subject).to include(crv: 'Ed25519', kty: 'OKP') + expect(subject.keys).to eq(%i[kty crv x kid]) + expect(subject[:x].size).to eq(43) + expect(subject[:kid].size).to eq(43) + end + end + context 'when private key is asked for' do + let(:key) { private_key } + let(:options) { { include_private: true } } + it 'exports the private key' do + expect(subject).to include(crv: 'Ed25519', kty: 'OKP') + expect(subject.keys).to eq(%i[kty crv x d kid]) + expect(subject[:x].size).to eq(43) + expect(subject[:d].size).to eq(43) + expect(subject[:kid].size).to eq(43) + end + end + end + + describe '.import' do + subject { described_class.import(import_data) } + + context 'when exported public key is given' do + let(:import_data) { described_class.new(public_key).export } + it 'creates a new instance of the class' do + expect(subject.private?).to eq(false) + expect(subject.verify_key).to be_a(RbNaCl::Signatures::Ed25519::VerifyKey) + expect(subject.signing_key).to eq(nil) + expect(subject.verify_key.to_bytes).to eq(public_key.to_bytes) + expect(subject.kid).to eq(import_data[:kid]) + end + end + + context 'when exported private key is given' do + let(:import_data) { described_class.new(private_key).export(include_private: true) } + it 'creates a new instance of the class' do + expect(subject.private?).to eq(true) + expect(subject.verify_key).to be_a(RbNaCl::Signatures::Ed25519::VerifyKey) + expect(subject.signing_key).to be_a(RbNaCl::Signatures::Ed25519::SigningKey) + expect(subject.verify_key.to_bytes).to eq(public_key.to_bytes) + expect(subject.kid).to eq(import_data[:kid]) + end + end + + context 'when JWK is given' do + let(:import_data) { described_class.new(private_key) } + it 'creates a new instance of the class' do + expect(subject.private?).to eq(true) + expect(subject.verify_key).to be_a(RbNaCl::Signatures::Ed25519::VerifyKey) + expect(subject.signing_key).to be_a(RbNaCl::Signatures::Ed25519::SigningKey) + expect(subject.verify_key.to_bytes).to eq(public_key.to_bytes) + expect(subject.kid).to eq(import_data[:kid]) + end + end + end +end diff --git a/spec/jwk/rsa_spec.rb b/spec/jwk/rsa_spec.rb index b888e6b0..234e3458 100644 --- a/spec/jwk/rsa_spec.rb +++ b/spec/jwk/rsa_spec.rb @@ -23,6 +23,14 @@ end end + describe '#keypair' do + subject(:jwk) { described_class.new(rsa_key) } + + it 'warns to stderr' do + expect(jwk.keypair).to eq(rsa_key) + end + end + describe '#export' do subject { described_class.new(keypair).export }