Skip to content

Commit

Permalink
EC keys and openssl 3.0 support
Browse files Browse the repository at this point in the history
  • Loading branch information
anakinj committed Jul 27, 2022
1 parent e892a7e commit 62e2a33
Show file tree
Hide file tree
Showing 11 changed files with 75 additions and 63 deletions.
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,14 @@ puts decoded_token
* ES256K - ECDSA using P-256K and SHA-256

```ruby
ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
ecdsa_key.generate_key
ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
ecdsa_public.private_key = nil
ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')

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

# eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg
puts token

decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' }
decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' }

# Array
# [
Expand Down
90 changes: 64 additions & 26 deletions lib/jwt/jwk/ec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

module JWT
module JWK
class EC < KeyBase
class EC < KeyBase # rubocop:disable Metrics/ClassLength
extend Forwardable
def_delegators :keypair, :public_key

Expand Down Expand Up @@ -121,31 +121,69 @@ def jwk_attrs(jwk_data, attrs)
end
end

def ec_pkey(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
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)

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 ec_pkey(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)
Expand Down
3 changes: 0 additions & 3 deletions spec/fixtures/certs/ec256-private.pem
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIJmVse5uPfj6B4TcXrUAvf9/8pJh+KrKKYLNcmOnp/vPoAoGCCqGSM49
AwEHoUQDQgAEAr+WbDE5VtIDGhtYMxvEc6cMsDBc/DX1wuhIMu8dQzOLSt0tpqK9
Expand Down
3 changes: 0 additions & 3 deletions spec/fixtures/certs/ec256-wrong-private.pem
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEICfA4AaomONdmPTzeyrx5U/jugYXTERyb5U3ETTv7Hx7oAcGBSuBBAAK
oUQDQgAEPmuXZT3jpJnEMVPOW6RMsmxeGLOCE1PN6fwvUwOsxv7YnyoQ5/bpo64n
Expand Down
3 changes: 0 additions & 3 deletions spec/fixtures/certs/ec384-private.pem
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDDxOljqUKw9YNhkluSJIBAYO1YXcNtS+vckd5hpTZ5toxsOlwbmyrnU
Tn+D5Xma1m2gBwYFK4EEACKhZANiAASQwYTiRvXu1hMHceSosMs/8uf50sJI3jvK
Expand Down
3 changes: 0 additions & 3 deletions spec/fixtures/certs/ec384-wrong-private.pem
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDAfZW47dSKnC5JkSVOk1ERxCIi/IJ1p1WBnVGx4hnrNHy+dxtaZJaF+
YLInFQ/QbYegBwYFK4EEACKhZANiAAQwXkx4BFBGLXbzl5yVrfxK7er8hSi38iDE
Expand Down
3 changes: 0 additions & 3 deletions spec/fixtures/certs/ec512-private.pem
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIB0/+ffxEj7j62xvGaB5pvzk888e412ESO/EK/K0QlS9dSF8+Rj1rG
zqpRB8fvDnoe8xdmkW/W5GKzojMyv7YQYumgBwYFK4EEACOhgYkDgYYABAEw74Yw
Expand Down
3 changes: 0 additions & 3 deletions spec/fixtures/certs/ec512-wrong-private.pem
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIHbAgEBBEG/KbA2oCbiCT6L3V8XSz2WKBy0XhGvIFbl/ZkXIXnkYt+1B7wViSVo
KCHuMFsi6xU/5nE1EuDG2UsQJmKeAMkIOKAHBgUrgQQAI6GBiQOBhgAEAG0TFWe5
Expand Down
7 changes: 2 additions & 5 deletions spec/integration/readme_examples_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,10 @@
end

it 'ECDSA' do
ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1'
ecdsa_key.generate_key
ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key
ecdsa_public.private_key = nil
ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1')

token = JWT.encode payload, ecdsa_key, 'ES256'
decoded_token = JWT.decode token, ecdsa_public, true, algorithm: 'ES256'
decoded_token = JWT.decode token, ecdsa_key, true, algorithm: 'ES256'

expect(decoded_token).to eq [
{ 'data' => 'test' },
Expand Down
10 changes: 5 additions & 5 deletions spec/jwk/ec_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

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

describe '.new' do
subject { described_class.new(keypair) }
Expand All @@ -15,7 +15,7 @@
end

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

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

context 'when keypair is private' do
let(:include_private) { true }
Expand Down Expand Up @@ -110,7 +110,7 @@

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

it 'returns a hash with the public parts of the key' do
Expand Down
6 changes: 2 additions & 4 deletions spec/jwt_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@
%w[ES256 ES384 ES512 ES256K].each do |alg|
context "alg: #{alg}" do
before(:each) do
data[alg] = JWT.encode payload, data["#{alg}_private"], alg
data[alg] = JWT.encode(payload, data["#{alg}_private"], alg)
end

let(:wrong_key) { OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec256-wrong-public.pem'))) }
Expand Down Expand Up @@ -343,15 +343,13 @@
end

it 'ECDSA curve_name should raise JWT::IncorrectAlgorithm' do
key = OpenSSL::PKey::EC.new 'secp256k1'
key.generate_key
key = OpenSSL::PKey::EC.generate('secp256k1')

expect do
JWT.encode payload, key, 'ES256'
end.to raise_error JWT::IncorrectAlgorithm

token = JWT.encode payload, data['ES256_private'], 'ES256'
key.private_key = nil

expect do
JWT.decode token, key
Expand Down

0 comments on commit 62e2a33

Please sign in to comment.