Skip to content

Commit 62e2a33

Browse files
committed
EC keys and openssl 3.0 support
1 parent e892a7e commit 62e2a33

File tree

11 files changed

+75
-63
lines changed

11 files changed

+75
-63
lines changed

README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,14 @@ puts decoded_token
135135
* ES256K - ECDSA using P-256K and SHA-256
136136

137137
```ruby
138-
ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
139-
ecdsa_key.generate_key
140-
ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
141-
ecdsa_public.private_key = nil
138+
ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
142139

143140
token = JWT.encode payload, ecdsa_key, 'ES256'
144141

145142
# eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
146143
puts token
147144

148-
decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
145+
decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }
149146

150147
# Array
151148
# [

lib/jwt/jwk/ec.rb

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
module JWT
66
module JWK
7-
class EC < KeyBase
7+
class EC < KeyBase # rubocop:disable Metrics/ClassLength
88
extend Forwardable
99
def_delegators :keypair, :public_key
1010

@@ -121,31 +121,69 @@ def jwk_attrs(jwk_data, attrs)
121121
end
122122
end
123123

124-
def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
125-
curve = to_openssl_curve(jwk_crv)
126-
127-
x_octets = decode_octets(jwk_x)
128-
y_octets = decode_octets(jwk_y)
129-
130-
key = OpenSSL::PKey::EC.new(curve)
131-
132-
# The details of the `Point` instantiation are covered in:
133-
# - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
134-
# - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
135-
# - https://tools.ietf.org/html/rfc5480#section-2.2
136-
# - https://www.secg.org/SEC1-Ver-1.0.pdf
137-
# Section 2.3.3 of the last of these references specifies that the
138-
# encoding of an uncompressed point consists of the byte `0x04` followed
139-
# by the x value then the y value.
140-
point = OpenSSL::PKey::EC::Point.new(
141-
OpenSSL::PKey::EC::Group.new(curve),
142-
OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
143-
)
144-
145-
key.public_key = point
146-
key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
147-
148-
key
124+
if ::JWT.openssl_3?
125+
def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
126+
curve = to_openssl_curve(jwk_crv)
127+
128+
x_octets = decode_octets(jwk_x)
129+
y_octets = decode_octets(jwk_y)
130+
131+
point = OpenSSL::PKey::EC::Point.new(
132+
OpenSSL::PKey::EC::Group.new(curve),
133+
OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
134+
)
135+
136+
sequence = if jwk_d
137+
# https://datatracker.ietf.org/doc/html/rfc5915.html
138+
# ECPrivateKey ::= SEQUENCE {
139+
# version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
140+
# privateKey OCTET STRING,
141+
# parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
142+
# publicKey [1] BIT STRING OPTIONAL
143+
# }
144+
145+
OpenSSL::ASN1::Sequence([
146+
OpenSSL::ASN1::Integer(1),
147+
OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
148+
OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
149+
OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
150+
])
151+
else
152+
OpenSSL::ASN1::Sequence([
153+
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
154+
OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
155+
])
156+
end
157+
158+
OpenSSL::PKey::EC.new(sequence.to_der)
159+
end
160+
else
161+
def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
162+
curve = to_openssl_curve(jwk_crv)
163+
164+
x_octets = decode_octets(jwk_x)
165+
y_octets = decode_octets(jwk_y)
166+
167+
key = OpenSSL::PKey::EC.new(curve)
168+
169+
# The details of the `Point` instantiation are covered in:
170+
# - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
171+
# - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
172+
# - https://tools.ietf.org/html/rfc5480#section-2.2
173+
# - https://www.secg.org/SEC1-Ver-1.0.pdf
174+
# Section 2.3.3 of the last of these references specifies that the
175+
# encoding of an uncompressed point consists of the byte `0x04` followed
176+
# by the x value then the y value.
177+
point = OpenSSL::PKey::EC::Point.new(
178+
OpenSSL::PKey::EC::Group.new(curve),
179+
OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
180+
)
181+
182+
key.public_key = point
183+
key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
184+
185+
key
186+
end
149187
end
150188

151189
def decode_octets(jwk_data)

spec/fixtures/certs/ec256-private.pem

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
-----BEGIN EC PARAMETERS-----
2-
BggqhkjOPQMBBw==
3-
-----END EC PARAMETERS-----
41
-----BEGIN EC PRIVATE KEY-----
52
MHcCAQEEIJmVse5uPfj6B4TcXrUAvf9/8pJh+KrKKYLNcmOnp/vPoAoGCCqGSM49
63
AwEHoUQDQgAEAr+WbDE5VtIDGhtYMxvEc6cMsDBc/DX1wuhIMu8dQzOLSt0tpqK9

spec/fixtures/certs/ec256-wrong-private.pem

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
-----BEGIN EC PARAMETERS-----
2-
BgUrgQQACg==
3-
-----END EC PARAMETERS-----
41
-----BEGIN EC PRIVATE KEY-----
52
MHQCAQEEICfA4AaomONdmPTzeyrx5U/jugYXTERyb5U3ETTv7Hx7oAcGBSuBBAAK
63
oUQDQgAEPmuXZT3jpJnEMVPOW6RMsmxeGLOCE1PN6fwvUwOsxv7YnyoQ5/bpo64n

spec/fixtures/certs/ec384-private.pem

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
-----BEGIN EC PARAMETERS-----
2-
BgUrgQQAIg==
3-
-----END EC PARAMETERS-----
41
-----BEGIN EC PRIVATE KEY-----
52
MIGkAgEBBDDxOljqUKw9YNhkluSJIBAYO1YXcNtS+vckd5hpTZ5toxsOlwbmyrnU
63
Tn+D5Xma1m2gBwYFK4EEACKhZANiAASQwYTiRvXu1hMHceSosMs/8uf50sJI3jvK

spec/fixtures/certs/ec384-wrong-private.pem

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
-----BEGIN EC PARAMETERS-----
2-
BgUrgQQAIg==
3-
-----END EC PARAMETERS-----
41
-----BEGIN EC PRIVATE KEY-----
52
MIGkAgEBBDAfZW47dSKnC5JkSVOk1ERxCIi/IJ1p1WBnVGx4hnrNHy+dxtaZJaF+
63
YLInFQ/QbYegBwYFK4EEACKhZANiAAQwXkx4BFBGLXbzl5yVrfxK7er8hSi38iDE

spec/fixtures/certs/ec512-private.pem

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
-----BEGIN EC PARAMETERS-----
2-
BgUrgQQAIw==
3-
-----END EC PARAMETERS-----
41
-----BEGIN EC PRIVATE KEY-----
52
MIHcAgEBBEIB0/+ffxEj7j62xvGaB5pvzk888e412ESO/EK/K0QlS9dSF8+Rj1rG
63
zqpRB8fvDnoe8xdmkW/W5GKzojMyv7YQYumgBwYFK4EEACOhgYkDgYYABAEw74Yw

spec/fixtures/certs/ec512-wrong-private.pem

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
-----BEGIN EC PARAMETERS-----
2-
BgUrgQQAIw==
3-
-----END EC PARAMETERS-----
41
-----BEGIN EC PRIVATE KEY-----
52
MIHbAgEBBEG/KbA2oCbiCT6L3V8XSz2WKBy0XhGvIFbl/ZkXIXnkYt+1B7wViSVo
63
KCHuMFsi6xU/5nE1EuDG2UsQJmKeAMkIOKAHBgUrgQQAI6GBiQOBhgAEAG0TFWe5

spec/integration/readme_examples_spec.rb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,10 @@
5353
end
5454

5555
it 'ECDSA' do
56-
ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
57-
ecdsa_key.generate_key
58-
ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
59-
ecdsa_public.private_key = nil
56+
ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')
6057

6158
token = JWT.encode payload, ecdsa_key, 'ES256'
62-
decoded_token = JWT.decode token, ecdsa_public, true, algorithm: 'ES256'
59+
decoded_token = JWT.decode token, ecdsa_key, true, algorithm: 'ES256'
6360

6461
expect(decoded_token).to eq [
6562
{ 'data' => 'test' },

spec/jwk/ec_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
RSpec.describe JWT::JWK::EC do
4-
let(:ec_key) { OpenSSL::PKey::EC.new('secp384r1').generate_key }
4+
let(:ec_key) { OpenSSL::PKey::EC.generate('secp384r1') }
55

66
describe '.new' do
77
subject { described_class.new(keypair) }
@@ -15,7 +15,7 @@
1515
end
1616

1717
context 'when a keypair with only public key is given' do
18-
let(:keypair) { OpenSSL::PKey::EC.new(ec_key.public_key.group).tap { |ec| ec.public_key = ec_key.public_key } }
18+
let(:keypair) { OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec256-public.pem'))) }
1919
it 'creates an instance of the class' do
2020
expect(subject).to be_a described_class
2121
expect(subject.private?).to eq false
@@ -41,7 +41,7 @@
4141
end
4242

4343
context 'when keypair with public key is exported' do
44-
let(:keypair) { ec_key.tap { |x| x.private_key = nil } }
44+
let(:keypair) { OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec256-public.pem'))) }
4545
it 'returns a hash with the public parts of the key' do
4646
expect(subject).to be_a Hash
4747
expect(subject).to include(:kty, :kid, :x, :y)
@@ -79,7 +79,7 @@
7979
['P-256', 'P-384', 'P-521', 'P-256K'].each do |crv|
8080
context "when crv=#{crv}" do
8181
let(:openssl_curve) { JWT::JWK::EC.to_openssl_curve(crv) }
82-
let(:ec_key) { OpenSSL::PKey::EC.new(openssl_curve).generate_key }
82+
let(:ec_key) { OpenSSL::PKey::EC.generate(openssl_curve) }
8383

8484
context 'when keypair is private' do
8585
let(:include_private) { true }
@@ -110,7 +110,7 @@
110110

111111
context 'when keypair is public' do
112112
context 'returns a public key' do
113-
let(:keypair) { ec_key.tap { |x| x.private_key = nil } }
113+
let(:keypair) { OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec256-public.pem'))) }
114114
let(:params) { exported_key }
115115

116116
it 'returns a hash with the public parts of the key' do

0 commit comments

Comments
 (0)