Skip to content

Commit

Permalink
Merge pull request #285 from creditkudos/feature/add-rsa-pss-support
Browse files Browse the repository at this point in the history
Add RSASSA-PSS signature signing support
  • Loading branch information
excpt committed Sep 27, 2018
2 parents 5ca149b + df315b5 commit f60d78a
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 4 deletions.
29 changes: 28 additions & 1 deletion README.md
Expand Up @@ -176,7 +176,34 @@ decoded_token = JWT.decode token, public_key, true, { algorithm: 'ED25519' }

**RSASSA-PSS**

Not implemented.
In order to use this algorithm you need to add the `openssl` gem to you `Gemfile` with a version greater or equal to `2.1`.

```ruby
gem 'openssl', '~> 2.1'
```

* PS256 - RSASSA-PSS using SHA-256 hash algorithm
* PS384 - RSASSA-PSS using SHA-384 hash algorithm
* PS512 - RSASSA-PSS using SHA-512 hash algorithm

```ruby
rsa_private = OpenSSL::PKey::RSA.generate 2048
rsa_public = rsa_private.public_key

token = JWT.encode payload, rsa_private, 'PS256'

# eyJhbGciOiJQUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.KEmqagMUHM-NcmXo6818ZazVTIAkn9qU9KQFT1c5Iq91n0KRpAI84jj4ZCdkysDlWokFs3Dmn4MhcXP03oJKLFgnoPL40_Wgg9iFr0jnIVvnMUp1kp2RFUbL0jqExGTRA3LdAhuvw6ZByGD1bkcWjDXygjQw-hxILrT1bENjdr0JhFd-cB0-ps5SB0mwhFNcUw-OM3Uu30B1-mlFaelUY8jHJYKwLTZPNxHzndt8RGXF8iZLp7dGb06HSCKMcVzhASGMH4ZdFystRe2hh31cwcvnl-Eo_D4cdwmpN3Abhk_8rkxawQJR3duh8HNKc4AyFPo7SabEaSu2gLnLfN3yfg
puts token

decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'PS256' }

# Array
# [
# {"data"=>"test"}, # payload
# {"alg"=>"PS256"} # header
# ]
puts decoded_token
```

## Support for reserved claim names
JSON Web Token defines some reserved claim names and defines how they should be
Expand Down
43 changes: 43 additions & 0 deletions lib/jwt/algos/ps.rb
@@ -0,0 +1,43 @@
module JWT
module Algos
module Ps
# RSASSA-PSS signing algorithms

module_function

SUPPORTED = %w[PS256 PS384 PS512].freeze

def sign(to_sign)
require_openssl!

algorithm, msg, key = to_sign.values

key_class = key.class

raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance." if key_class == String

translated_algorithm = algorithm.sub('PS', 'sha')

key.sign_pss(translated_algorithm, msg, salt_length: :max, mgf1_hash: translated_algorithm)
end

def verify(to_verify)
require_openssl!

SecurityUtils.verify_ps(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
end

def require_openssl!
if Object.const_defined?('OpenSSL')
major, minor = OpenSSL::VERSION.split('.').first(2)

unless major.to_i >= 2 && minor.to_i >= 1
raise JWT::RequiredDependencyError, "You currently have OpenSSL #{OpenSSL::VERSION}. PS support requires >= 2.1"
end
else
raise JWT::RequiredDependencyError, 'PS signing requires OpenSSL +2.1'
end
end
end
end
end
5 changes: 3 additions & 2 deletions lib/jwt/error.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true

module JWT
EncodeError = Class.new(StandardError)
DecodeError = Class.new(StandardError)
EncodeError = Class.new(StandardError)
DecodeError = Class.new(StandardError)
RequiredDependencyError = Class.new(StandardError)

VerificationError = Class.new(DecodeError)
ExpiredSignature = Class.new(DecodeError)
Expand Down
6 changes: 6 additions & 0 deletions lib/jwt/security_utils.rb
Expand Up @@ -20,6 +20,12 @@ def verify_rsa(algorithm, public_key, signing_input, signature)
public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
end

def verify_ps(algorithm, public_key, signing_input, signature)
formatted_algorithm = algorithm.sub('PS', 'sha')

public_key.verify_pss(formatted_algorithm, signature, signing_input, salt_length: :auto, mgf1_hash: formatted_algorithm)
end

def asn1_to_raw(signature, public_key)
byte_size = (public_key.group.degree + 7) / 8
OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
Expand Down
2 changes: 2 additions & 0 deletions lib/jwt/signature.rb
Expand Up @@ -6,6 +6,7 @@
require 'jwt/algos/eddsa'
require 'jwt/algos/ecdsa'
require 'jwt/algos/rsa'
require 'jwt/algos/ps'
require 'jwt/algos/unsupported'
begin
require 'rbnacl'
Expand All @@ -23,6 +24,7 @@ module Signature
Algos::Ecdsa,
Algos::Rsa,
Algos::Eddsa,
Algos::Ps,
Algos::Unsupported
].freeze
ToSign = Struct.new(:algorithm, :msg, :key)
Expand Down
2 changes: 2 additions & 0 deletions ruby-jwt.gemspec
Expand Up @@ -29,4 +29,6 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'codeclimate-test-reporter'
spec.add_development_dependency 'codacy-coverage'
spec.add_development_dependency 'rbnacl'
# RSASSA-PSS support provided by OpenSSL +2.1
spec.add_development_dependency 'openssl', '~> 2.1'
end
13 changes: 13 additions & 0 deletions spec/integration/readme_examples_spec.rb
Expand Up @@ -56,6 +56,19 @@
{ 'alg' => 'ES256' }
]
end

it 'RSASSA-PSS' do
rsa_private = OpenSSL::PKey::RSA.generate 2048
rsa_public = rsa_private.public_key

token = JWT.encode payload, rsa_private, 'PS256'
decoded_token = JWT.decode token, rsa_public, true, algorithm: 'PS256'

expect(decoded_token).to eq [
{ 'data' => 'test' },
{ 'alg' => 'PS256' }
]
end
end

context 'claims' do
Expand Down
54 changes: 53 additions & 1 deletion spec/jwt_spec.rb
Expand Up @@ -31,7 +31,10 @@
'RS512' => 'eyJhbGciOiJSUzUxMiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.LIIAUEuCkGNdpYguOO5LoW4rZ7ED2POJrB0pmEAAchyTdIK4HKh1jcLxc6KyGwZv40njCgub3y72q6vcQTn7oD0zWFCVQRIDW1911Ii2hRNHuigiPUnrnZh1OQ6z65VZRU6GKs8omoBGU9vrClBU0ODqYE16KxYmE_0n4Xw2h3D_L1LF0IAOtDWKBRDa3QHwZRM9sHsHNsBuD5ye9KzDYN1YALXj64LBfA-DoCKfpVAm9NkRPOyzjR2X2C3TomOSJgqWIVHJucudKDDAZyEbO4RA5pI-UFYy1370p9bRajvtDyoBuLDCzoSkMyQ4L2DnLhx5CbWcnD7Cd3GUmnjjTA',
'ES256' => '',
'ES384' => '',
'ES512' => ''
'ES512' => '',
'PS256' => '',
'PS384' => '',
'PS512' => ''
}
end

Expand Down Expand Up @@ -205,6 +208,55 @@
end
end

%w[PS256 PS384 PS512].each do |alg|
context "alg: #{alg}" do
before(:each) do
data[alg] = JWT.encode payload, data[:rsa_private], alg
end

let(:wrong_key) { data[:wrong_rsa_public] }

it 'should generate a valid token' do
token = data[alg]

header, body, signature = token.split('.')

expect(header).to eql(Base64.strict_encode64({ alg: alg }.to_json))
expect(body).to eql(Base64.strict_encode64(payload.to_json))

# Validate signature is made of up header and body of JWT
translated_alg = alg.gsub('PS', 'sha')
valid_signature = data[:rsa_public].verify_pss(
translated_alg,
JWT::Decode.base64url_decode(signature),
[header, body].join('.'),
salt_length: :auto,
mgf1_hash: translated_alg
)
expect(valid_signature).to be true
end

it 'should decode a valid token' do
jwt_payload, header = JWT.decode data[alg], data[:rsa_public], true, algorithm: alg

expect(header['alg']).to eq alg
expect(jwt_payload).to eq payload
end

it 'wrong key should raise JWT::DecodeError' do
expect do
JWT.decode data[alg], wrong_key
end.to raise_error JWT::DecodeError
end

it 'wrong key and verify = false should not raise JWT::DecodeError' do
expect do
JWT.decode data[alg], wrong_key, false
end.not_to raise_error
end
end
end

context 'Invalid' do
it 'algorithm should raise NotImplementedError' do
expect do
Expand Down

0 comments on commit f60d78a

Please sign in to comment.