Skip to content

Commit d6ee141

Browse files
committed
JWK OKP Ed25519 support using rbnacl
- Prepare to deprecate the ::JWT::JWK::[RSA/EC/HMAC]#keypair method in favor of methods describing the use
1 parent fce8bbc commit d6ee141

File tree

14 files changed

+377
-55
lines changed

14 files changed

+377
-55
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
**Features:**
88

9+
- Support OKP (Ed25519) keys for JWKs [#540](https://github.com/jwt/ruby-jwt/pull/540) ([@anakinj](https://github.com/anakinj)).
910
- Your contribution here
1011
- 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))
1112

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ end
569569

570570
### JSON Web Key (JWK)
571571

572-
JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC and HMAC keys.
572+
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.
573573

574574
To encode a JWT using your JWK:
575575

@@ -579,7 +579,7 @@ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
579579

580580
# Encoding
581581
payload = { data: 'data' }
582-
token = JWT.encode(payload, jwk.keypair, jwk[:alg], kid: jwk[:kid])
582+
token = JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid])
583583

584584
# JSON Web Key Set for advertising your signing keys
585585
jwks_hash = JWT::JWK::Set.new(jwk).export
@@ -653,8 +653,8 @@ jwk_hash = jwk.export
653653
jwk_hash_with_private_key = jwk.export(include_private: true)
654654

655655
# Export as OpenSSL key
656-
public_key = jwk.public_key
657-
private_key = jwk.keypair if jwk.private?
656+
public_key = jwk.verify_key
657+
private_key = jwk.signing_key if jwk.private?
658658

659659
# You can also import and export entire JSON Web Key Sets
660660
jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] }

lib/jwt/jwk.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,4 @@ def generate_mappings
5252
require_relative 'jwk/ec'
5353
require_relative 'jwk/rsa'
5454
require_relative 'jwk/hmac'
55+
require_relative 'jwk/okp_rbnacl' if ::JWT.rbnacl?

lib/jwt/jwk/ec.rb

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
module JWT
66
module JWK
77
class EC < KeyBase # rubocop:disable Metrics/ClassLength
8-
extend Forwardable
9-
def_delegators :keypair, :public_key
10-
118
KTY = 'EC'
129
KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
1310
BINARY = 2
@@ -24,17 +21,29 @@ def initialize(key, params = nil, options = {})
2421
key_params = extract_key_params(key)
2522

2623
params = params.transform_keys(&:to_sym)
27-
check_jwk(key_params, params)
24+
check_jwk_params!(key_params, params)
2825

2926
super(options, key_params.merge(params))
3027
end
3128

3229
def keypair
33-
@keypair ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
30+
ec_key
3431
end
3532

3633
def private?
37-
keypair.private_key?
34+
ec_key.private_key?
35+
end
36+
37+
def signing_key
38+
ec_key
39+
end
40+
41+
def verify_key
42+
ec_key
43+
end
44+
45+
def public_key
46+
ec_key
3847
end
3948

4049
def members
@@ -48,7 +57,7 @@ def export(options = {})
4857
end
4958

5059
def key_digest
51-
_crv, x_octets, y_octets = keypair_components(keypair)
60+
_crv, x_octets, y_octets = keypair_components(ec_key)
5261
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
5362
OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
5463
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
@@ -64,12 +73,16 @@ def []=(key, value)
6473

6574
private
6675

76+
def ec_key
77+
@ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
78+
end
79+
6780
def extract_key_params(key)
6881
case key
6982
when JWT::JWK::EC
7083
key.export(include_private: true)
7184
when OpenSSL::PKey::EC # Accept OpenSSL key as input
72-
@keypair = key # Preserve the object to avoid recreation
85+
@ec_key = key # Preserve the object to avoid recreation
7386
parse_ec_key(key)
7487
when Hash
7588
key.transform_keys(&:to_sym)
@@ -78,10 +91,10 @@ def extract_key_params(key)
7891
end
7992
end
8093

81-
def check_jwk(keypair, params)
94+
def check_jwk_params!(key_params, params)
8295
raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty?
83-
raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
84-
raise JWT::JWKError, 'Key format is invalid for EC' unless keypair[:crv] && keypair[:x] && keypair[:y]
96+
raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
97+
raise JWT::JWKError, 'Key format is invalid for EC' unless key_params[:crv] && key_params[:x] && key_params[:y]
8598
end
8699

87100
def keypair_components(ec_keypair)

lib/jwt/jwk/hmac.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def initialize(key, params = nil, options = {})
2424
end
2525

2626
def keypair
27-
self[:k]
27+
secret
2828
end
2929

3030
def private?
@@ -35,6 +35,14 @@ def public_key
3535
nil
3636
end
3737

38+
def verify_key
39+
secret
40+
end
41+
42+
def signing_key
43+
secret
44+
end
45+
3846
# See https://tools.ietf.org/html/rfc7517#appendix-A.3
3947
def export(options = {})
4048
exported = parameters.clone
@@ -46,8 +54,6 @@ def members
4654
HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
4755
end
4856

49-
alias signing_key keypair # for backwards compatibility
50-
5157
def key_digest
5258
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
5359
OpenSSL::ASN1::UTF8String.new(KTY)])
@@ -64,6 +70,10 @@ def []=(key, value)
6470

6571
private
6672

73+
def secret
74+
self[:k]
75+
end
76+
6777
def extract_key_params(key)
6878
case key
6979
when JWT::JWK::HMAC

lib/jwt/jwk/key_finder.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def key_for(kid)
2323
raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
2424
raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
2525

26-
jwk.keypair
26+
jwk.verify_key
2727
end
2828

2929
private

lib/jwt/jwk/okp_rbnacl.rb

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# frozen_string_literal: true
2+
3+
module JWT
4+
module JWK
5+
class OKPRbNaCl < KeyBase
6+
KTY = 'OKP'
7+
KTYS = [KTY, JWT::JWK::OKPRbNaCl, RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey].freeze
8+
OKP_PUBLIC_KEY_ELEMENTS = %i[kty n x].freeze
9+
OKP_PRIVATE_KEY_ELEMENTS = %i[d].freeze
10+
11+
def initialize(key, params = nil, options = {})
12+
params ||= {}
13+
14+
# For backwards compatibility when kid was a String
15+
params = { kid: params } if params.is_a?(String)
16+
17+
key_params = extract_key_params(key)
18+
19+
params = params.transform_keys(&:to_sym)
20+
check_jwk_params!(key_params, params)
21+
super(options, key_params.merge(params))
22+
end
23+
24+
def verify_key
25+
return @verify_key if defined?(@verify_key)
26+
27+
@verify_key = verify_key_from_parameters
28+
end
29+
30+
def signing_key
31+
return @signing_key if defined?(@signing_key)
32+
33+
@signing_key = signing_key_from_parameters
34+
end
35+
36+
def key_digest
37+
Thumbprint.new(self).to_s
38+
end
39+
40+
def private?
41+
!signing_key.nil?
42+
end
43+
44+
def members
45+
OKP_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
46+
end
47+
48+
def export(options = {})
49+
exported = parameters.clone
50+
exported.reject! { |k, _| OKP_PRIVATE_KEY_ELEMENTS.include?(k) } unless private? && options[:include_private] == true
51+
exported
52+
end
53+
54+
private
55+
56+
def extract_key_params(key)
57+
case key
58+
when JWT::JWK::KeyBase
59+
key.export(include_private: true)
60+
when RbNaCl::Signatures::Ed25519::SigningKey
61+
@signing_key = key
62+
@verify_key = key.verify_key
63+
parse_okp_key_params(@verify_key, @signing_key)
64+
when RbNaCl::Signatures::Ed25519::VerifyKey
65+
@signing_key = nil
66+
@verify_key = key
67+
parse_okp_key_params(@verify_key)
68+
when Hash
69+
key.transform_keys(&:to_sym)
70+
else
71+
raise ArgumentError, 'key must be of type RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey or Hash with key parameters'
72+
end
73+
end
74+
75+
def check_jwk_params!(key_params, _given_params)
76+
raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
77+
end
78+
79+
def parse_okp_key_params(verify_key, signing_key = nil)
80+
params = {
81+
kty: KTY,
82+
crv: 'Ed25519',
83+
x: ::JWT::Base64.url_encode(verify_key.to_bytes)
84+
}
85+
86+
if signing_key
87+
params[:d] = ::JWT::Base64.url_encode(signing_key.to_bytes)
88+
end
89+
90+
params
91+
end
92+
93+
def verify_key_from_parameters
94+
RbNaCl::Signatures::Ed25519::VerifyKey.new(::JWT::Base64.url_decode(self[:x]))
95+
end
96+
97+
def signing_key_from_parameters
98+
return nil unless self[:d]
99+
100+
RbNaCl::Signatures::Ed25519::SigningKey.new(::JWT::Base64.url_decode(self[:d]))
101+
end
102+
103+
class << self
104+
def import(jwk_data)
105+
new(jwk_data)
106+
end
107+
end
108+
end
109+
end
110+
end

lib/jwt/jwk/rsa.rb

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,29 @@ def initialize(key, params = nil, options = {})
2222
key_params = extract_key_params(key)
2323

2424
params = params.transform_keys(&:to_sym)
25-
check_jwk(key_params, params)
25+
check_jwk_params!(key_params, params)
2626

2727
super(options, key_params.merge(params))
2828
end
2929

3030
def keypair
31-
@keypair ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty])))
31+
rsa_key
3232
end
3333

3434
def private?
35-
keypair.private?
35+
rsa_key.private?
3636
end
3737

3838
def public_key
39-
keypair.public_key
39+
rsa_key.public_key
40+
end
41+
42+
def signing_key
43+
rsa_key if private?
44+
end
45+
46+
def verify_key
47+
rsa_key.public_key
4048
end
4149

4250
def export(options = {})
@@ -65,12 +73,16 @@ def []=(key, value)
6573

6674
private
6775

76+
def rsa_key
77+
@rsa_key ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty])))
78+
end
79+
6880
def extract_key_params(key)
6981
case key
7082
when JWT::JWK::RSA
7183
key.export(include_private: true)
7284
when OpenSSL::PKey::RSA # Accept OpenSSL key as input
73-
@keypair = key # Preserve the object to avoid recreation
85+
@rsa_key = key # Preserve the object to avoid recreation
7486
parse_rsa_key(key)
7587
when Hash
7688
key.transform_keys(&:to_sym)
@@ -79,10 +91,10 @@ def extract_key_params(key)
7991
end
8092
end
8193

82-
def check_jwk(keypair, params)
94+
def check_jwk_params!(key_params, params)
8395
raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (RSA_KEY_ELEMENTS & params.keys).empty?
84-
raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
85-
raise JWT::JWKError, 'Key format is invalid for RSA' unless keypair[:n] && keypair[:e]
96+
raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
97+
raise JWT::JWKError, 'Key format is invalid for RSA' unless key_params[:n] && key_params[:e]
8698
end
8799

88100
def parse_rsa_key(key)

0 commit comments

Comments
 (0)