jwtb 2.0.0.beta2.bsk1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +20 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +5 -0
  6. data/.travis.yml +13 -0
  7. data/CHANGELOG.md +411 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE +7 -0
  10. data/Manifest +8 -0
  11. data/README.md +443 -0
  12. data/Rakefile +11 -0
  13. data/jwtb.gemspec +31 -0
  14. data/lib/jwtb.rb +67 -0
  15. data/lib/jwtb/decode.rb +45 -0
  16. data/lib/jwtb/default_options.rb +14 -0
  17. data/lib/jwtb/encode.rb +51 -0
  18. data/lib/jwtb/error.rb +15 -0
  19. data/lib/jwtb/signature.rb +146 -0
  20. data/lib/jwtb/verify.rb +84 -0
  21. data/lib/jwtb/version.rb +24 -0
  22. data/spec/fixtures/certs/ec256-private.pem +8 -0
  23. data/spec/fixtures/certs/ec256-public.pem +4 -0
  24. data/spec/fixtures/certs/ec256-wrong-private.pem +8 -0
  25. data/spec/fixtures/certs/ec256-wrong-public.pem +4 -0
  26. data/spec/fixtures/certs/ec384-private.pem +9 -0
  27. data/spec/fixtures/certs/ec384-public.pem +5 -0
  28. data/spec/fixtures/certs/ec384-wrong-private.pem +9 -0
  29. data/spec/fixtures/certs/ec384-wrong-public.pem +5 -0
  30. data/spec/fixtures/certs/ec512-private.pem +10 -0
  31. data/spec/fixtures/certs/ec512-public.pem +6 -0
  32. data/spec/fixtures/certs/ec512-wrong-private.pem +10 -0
  33. data/spec/fixtures/certs/ec512-wrong-public.pem +6 -0
  34. data/spec/fixtures/certs/rsa-1024-private.pem +15 -0
  35. data/spec/fixtures/certs/rsa-1024-public.pem +6 -0
  36. data/spec/fixtures/certs/rsa-2048-private.pem +27 -0
  37. data/spec/fixtures/certs/rsa-2048-public.pem +9 -0
  38. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +27 -0
  39. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +9 -0
  40. data/spec/fixtures/certs/rsa-4096-private.pem +51 -0
  41. data/spec/fixtures/certs/rsa-4096-public.pem +14 -0
  42. data/spec/integration/readme_examples_spec.rb +216 -0
  43. data/spec/jwtb/verify_spec.rb +190 -0
  44. data/spec/jwtb_spec.rb +233 -0
  45. data/spec/spec_helper.rb +28 -0
  46. metadata +225 -0
@@ -0,0 +1,11 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:test)
7
+
8
+ task default: :test
9
+ rescue LoadError
10
+ puts 'RSpec rake tasks not available. Please run "bundle install" to install missing dependencies.'
11
+ end
@@ -0,0 +1,31 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'jwtb/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'jwtb'
7
+ spec.version = JWTB.gem_version
8
+ spec.authors = [
9
+ 'Tim Rudat', 'Larry Salibra'
10
+ ]
11
+ spec.email = 'timrudat@gmail.com'
12
+ spec.summary = 'JSON Web Token implementation with Blockstack support in Ruby '
13
+ spec.description = 'A pure ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard with additional support for Blockstack.'
14
+ spec.homepage = 'http://github.com/blockstack/ruby-jwt-blockstack'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '~> 2.1'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = %w(lib)
22
+
23
+ spec.add_development_dependency 'bundler'
24
+ spec.add_development_dependency 'rake'
25
+ spec.add_development_dependency 'rspec'
26
+ spec.add_development_dependency 'simplecov'
27
+ spec.add_development_dependency 'simplecov-json'
28
+ spec.add_development_dependency 'codeclimate-test-reporter'
29
+ spec.add_development_dependency 'codacy-coverage'
30
+ spec.add_development_dependency 'rbnacl'
31
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ require 'base64'
3
+ require 'jwtb/decode'
4
+ require 'jwtb/default_options'
5
+ require 'jwtb/encode'
6
+ require 'jwtb/error'
7
+ require 'jwtb/signature'
8
+ require 'jwtb/verify'
9
+
10
+ # JSON Web Token implementation
11
+ #
12
+ # Should be up to date with the latest spec:
13
+ # https://tools.ietf.org/html/rfc7519
14
+ module JWTB
15
+ include JWTB::DefaultOptions
16
+
17
+ module_function
18
+
19
+ def decoded_segments(jwt, verify = true)
20
+ raise(JWTB::DecodeError, 'Nil JSON web token') unless jwt
21
+
22
+ decoder = Decode.new jwt, verify
23
+ decoder.decode_segments
24
+ end
25
+
26
+ def encode(payload, key, algorithm = 'HS256', header_fields = {})
27
+ encoder = Encode.new payload, key, algorithm, header_fields
28
+ encoder.segments
29
+ end
30
+
31
+ def decode(jwt, key = nil, verify = true, custom_options = {}, &keyfinder)
32
+ raise(JWTB::DecodeError, 'Nil JSON web token') unless jwt
33
+
34
+ merged_options = DEFAULT_OPTIONS.merge(custom_options)
35
+
36
+ decoder = Decode.new jwt, verify
37
+ header, payload, signature, signing_input = decoder.decode_segments
38
+ decode_verify_signature(key, header, payload, signature, signing_input, merged_options, &keyfinder) if verify
39
+
40
+ Verify.verify_claims(payload, merged_options)
41
+
42
+ raise(JWTB::DecodeError, 'Not enough or too many segments') unless header && payload
43
+
44
+ [payload, header]
45
+ end
46
+
47
+ def decode_verify_signature(key, header, payload, signature, signing_input, options, &keyfinder)
48
+ algo, key = signature_algorithm_and_key(header, payload, key, &keyfinder)
49
+
50
+ raise(JWTB::IncorrectAlgorithm, 'An algorithm must be specified') unless options[:algorithm]
51
+ raise(JWTB::IncorrectAlgorithm, 'Expected a different algorithm') unless algo == options[:algorithm]
52
+
53
+ Signature.verify(algo, key, signing_input, signature)
54
+ end
55
+
56
+ def signature_algorithm_and_key(header, payload, key, &keyfinder)
57
+ if keyfinder
58
+ key = if keyfinder.arity == 2
59
+ yield(header, payload)
60
+ else
61
+ yield(header)
62
+ end
63
+ raise JWTB::DecodeError, 'No verification key available' unless key
64
+ end
65
+ [header['alg'], key]
66
+ end
67
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+
4
+ # JWTB::Decode module
5
+ module JWTB
6
+ # Decoding logic for JWTB
7
+ class Decode
8
+ attr_reader :header, :payload, :signature
9
+
10
+ def self.base64url_decode(str)
11
+ str += '=' * (4 - str.length.modulo(4))
12
+ Base64.decode64(str.tr('-_', '+/'))
13
+ end
14
+
15
+ def initialize(jwt, verify)
16
+ @jwt = jwt
17
+ @verify = verify
18
+ end
19
+
20
+ def decode_segments
21
+ header_segment, payload_segment, crypto_segment = raw_segments(@jwt, @verify)
22
+ @header, @payload = decode_header_and_payload(header_segment, payload_segment)
23
+ @signature = Decode.base64url_decode(crypto_segment.to_s) if @verify
24
+ signing_input = [header_segment, payload_segment].join('.')
25
+ [@header, @payload, @signature, signing_input]
26
+ end
27
+
28
+ private
29
+
30
+ def raw_segments(jwt, verify)
31
+ segments = jwt.split('.')
32
+ required_num_segments = verify ? [3] : [2, 3]
33
+ raise(JWTB::DecodeError, 'Not enough or too many segments') unless required_num_segments.include? segments.length
34
+ segments
35
+ end
36
+
37
+ def decode_header_and_payload(header_segment, payload_segment)
38
+ header = JSON.parse(Decode.base64url_decode(header_segment))
39
+ payload = JSON.parse(Decode.base64url_decode(payload_segment))
40
+ [header, payload]
41
+ rescue JSON::ParserError
42
+ raise JWTB::DecodeError, 'Invalid segment encoding'
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ module JWTB
2
+ module DefaultOptions
3
+ DEFAULT_OPTIONS = {
4
+ verify_expiration: true,
5
+ verify_not_before: true,
6
+ verify_iss: false,
7
+ verify_iat: false,
8
+ verify_jti: false,
9
+ verify_aud: false,
10
+ verify_sub: false,
11
+ leeway: 0
12
+ }.freeze
13
+ end
14
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+
4
+ # JWTB::Encode module
5
+ module JWTB
6
+ # Encoding logic for JWTB
7
+ class Encode
8
+ attr_reader :payload, :key, :algorithm, :header_fields, :segments
9
+
10
+ def self.base64url_encode(str)
11
+ Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
12
+ end
13
+
14
+ def initialize(payload, key, algorithm, header_fields)
15
+ @payload = payload
16
+ @key = key
17
+ @algorithm = algorithm
18
+ @header_fields = header_fields
19
+ @segments = encode_segments
20
+ end
21
+
22
+ private
23
+
24
+ def encoded_header(algorithm, header_fields)
25
+ header = { 'alg' => algorithm }.merge(header_fields)
26
+ Encode.base64url_encode(JSON.generate(header))
27
+ end
28
+
29
+ def encoded_payload(payload)
30
+ raise InvalidPayload, 'exp claim must be an integer' if payload['exp'] && payload['exp'].is_a?(Time)
31
+ Encode.base64url_encode(JSON.generate(payload))
32
+ end
33
+
34
+ def encoded_signature(signing_input, key, algorithm)
35
+ if algorithm == 'none'
36
+ ''
37
+ else
38
+ signature = JWTB::Signature.sign(algorithm, signing_input, key)
39
+ Encode.base64url_encode(signature)
40
+ end
41
+ end
42
+
43
+ def encode_segments
44
+ segments = []
45
+ segments << encoded_header(@algorithm, @header_fields)
46
+ segments << encoded_payload(@payload)
47
+ segments << encoded_signature(segments.join('.'), @key, @algorithm)
48
+ segments.join('.')
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ module JWTB
3
+ class EncodeError < StandardError; end
4
+ class DecodeError < StandardError; end
5
+ class VerificationError < DecodeError; end
6
+ class ExpiredSignature < DecodeError; end
7
+ class IncorrectAlgorithm < DecodeError; end
8
+ class ImmatureSignature < DecodeError; end
9
+ class InvalidIssuerError < DecodeError; end
10
+ class InvalidIatError < DecodeError; end
11
+ class InvalidAudError < DecodeError; end
12
+ class InvalidSubError < DecodeError; end
13
+ class InvalidJtiError < DecodeError; end
14
+ class InvalidPayload < DecodeError; end
15
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+ require 'openssl'
3
+ begin
4
+ require 'rbnacl'
5
+ rescue LoadError
6
+ end
7
+
8
+ # JWTB::Signature module
9
+ module JWTB
10
+ # Signature logic for JWTB
11
+ module Signature
12
+ extend self
13
+
14
+ HMAC_ALGORITHMS = %w(HS256 HS512256 HS384 HS512).freeze
15
+ RSA_ALGORITHMS = %w(RS256 RS384 RS512).freeze
16
+ ECDSA_ALGORITHMS = %w(ES256 ES384 ES512 ES256K).freeze
17
+
18
+ NAMED_CURVES = {
19
+ 'prime256v1' => 'ES256',
20
+ 'secp384r1' => 'ES384',
21
+ 'secp521r1' => 'ES512',
22
+ 'secp256k1' => 'ES256K'
23
+ }.freeze
24
+
25
+ def sign(algorithm, msg, key)
26
+ if HMAC_ALGORITHMS.include?(algorithm)
27
+ sign_hmac(algorithm, msg, key)
28
+ elsif RSA_ALGORITHMS.include?(algorithm)
29
+ sign_rsa(algorithm, msg, key)
30
+ elsif ECDSA_ALGORITHMS.include?(algorithm)
31
+ sign_ecdsa(algorithm, msg, key)
32
+ else
33
+ raise NotImplementedError, 'Unsupported signing method'
34
+ end
35
+ end
36
+
37
+ def verify(algo, key, signing_input, signature)
38
+ verified = if HMAC_ALGORITHMS.include?(algo)
39
+ verify_hmac(algo, key, signing_input, signature)
40
+ elsif RSA_ALGORITHMS.include?(algo)
41
+ verify_rsa(algo, key, signing_input, signature)
42
+ elsif ECDSA_ALGORITHMS.include?(algo)
43
+ verify_ecdsa(algo, key, signing_input, signature)
44
+ else
45
+ raise JWTB::VerificationError, 'Algorithm not supported'
46
+ end
47
+
48
+ raise(JWTB::VerificationError, 'Signature verification raised') unless verified
49
+ rescue OpenSSL::PKey::PKeyError
50
+ raise JWTB::VerificationError, 'Signature verification raised'
51
+ ensure
52
+ OpenSSL.errors.clear
53
+ end
54
+
55
+ private
56
+
57
+ def sign_rsa(algorithm, msg, private_key)
58
+ raise EncodeError, "The given key is a #{private_key.class}. It has to be an OpenSSL::PKey::RSA instance." if private_key.class == String
59
+ private_key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
60
+ end
61
+
62
+ def sign_ecdsa(algorithm, msg, private_key)
63
+ key_algorithm = NAMED_CURVES[private_key.group.curve_name]
64
+ if algorithm != key_algorithm
65
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
66
+ end
67
+
68
+ digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha').sub('K', ''))
69
+ asn1_to_raw(private_key.dsa_sign_asn1(digest.digest(msg)), private_key)
70
+ end
71
+
72
+ def sign_hmac(algorithm, msg, key)
73
+ authenticator, padded_key = rbnacl_fixup(algorithm, key)
74
+ if authenticator && padded_key
75
+ authenticator.auth(padded_key, msg.encode('binary'))
76
+ else
77
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
78
+ end
79
+ end
80
+
81
+ def verify_rsa(algorithm, public_key, signing_input, signature)
82
+ public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
83
+ end
84
+
85
+ def verify_ecdsa(algorithm, public_key, signing_input, signature)
86
+ key_algorithm = NAMED_CURVES[public_key.group.curve_name]
87
+ if algorithm != key_algorithm
88
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
89
+ end
90
+
91
+ digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha').sub('K', ''))
92
+ public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key))
93
+ end
94
+
95
+ def verify_hmac(algorithm, public_key, signing_input, signature)
96
+ authenticator, padded_key = rbnacl_fixup(algorithm, public_key)
97
+ if authenticator && padded_key
98
+ begin
99
+ authenticator.verify(padded_key, signature.encode('binary'), signing_input.encode('binary'))
100
+ rescue RbNaCl::BadAuthenticatorError
101
+ false
102
+ end
103
+ else
104
+ secure_compare(signature, sign_hmac(algorithm, signing_input, public_key))
105
+ end
106
+ end
107
+
108
+ def asn1_to_raw(signature, public_key)
109
+ byte_size = (public_key.group.degree + 7) / 8
110
+ OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
111
+ end
112
+
113
+ def raw_to_asn1(signature, private_key)
114
+ byte_size = (private_key.group.degree + 7) / 8
115
+ r = signature[0..(byte_size - 1)]
116
+ s = signature[byte_size..-1] || ''
117
+ OpenSSL::ASN1::Sequence.new([r, s].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
118
+ end
119
+
120
+ def rbnacl_fixup(algorithm, key)
121
+ algorithm = algorithm.sub('HS', 'SHA').to_sym
122
+
123
+ return [] unless defined?(RbNaCl) && RbNaCl::HMAC.constants(false).include?(algorithm)
124
+
125
+ authenticator = RbNaCl::HMAC.const_get(algorithm)
126
+
127
+ # Fall back to OpenSSL for keys larger than 32 bytes.
128
+ return [] if key.bytesize > authenticator.key_bytes
129
+
130
+ [
131
+ authenticator,
132
+ key.bytes.fill(0, key.bytesize...authenticator.key_bytes).pack('C*')
133
+ ]
134
+ end
135
+
136
+ # From devise
137
+ # constant-time comparison algorithm to prevent timing attacks
138
+ def secure_compare(a, b)
139
+ return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize
140
+ l = a.unpack "C#{a.bytesize}"
141
+ res = 0
142
+ b.each_byte { |byte| res |= byte ^ l.shift }
143
+ res.zero?
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+ require 'jwtb/error'
3
+
4
+ module JWTB
5
+ # JWTB verify methods
6
+ class Verify
7
+ class << self
8
+ %w(verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub).each do |method_name|
9
+ define_method method_name do |payload, options|
10
+ new(payload, options).send(method_name)
11
+ end
12
+ end
13
+
14
+ def verify_claims(payload, options)
15
+ options.each do |key, val|
16
+ next unless key.to_s =~ /verify/
17
+ Verify.send(key, payload, options) if val
18
+ end
19
+ end
20
+ end
21
+
22
+ def initialize(payload, options)
23
+ @payload = payload
24
+ @options = options
25
+ end
26
+
27
+ def verify_aud
28
+ return unless (options_aud = @options[:aud])
29
+ raise(JWTB::InvalidAudError, "Invalid audience. Expected #{options_aud}, received #{@payload['aud'] || '<none>'}") if ([*@payload['aud']] & [*options_aud]).empty?
30
+ end
31
+
32
+ def verify_expiration
33
+ return unless @payload.include?('exp')
34
+ raise(JWTB::ExpiredSignature, 'Signature has expired') if @payload['exp'].to_i <= (Time.now.to_i - exp_leeway)
35
+ end
36
+
37
+ def verify_iat
38
+ return unless @payload.include?('iat')
39
+ raise(JWTB::InvalidIatError, 'Invalid iat') if !@payload['iat'].is_a?(Numeric) || @payload['iat'].to_f > (Time.now.to_f + iat_leeway)
40
+ end
41
+
42
+ def verify_iss
43
+ return unless (options_iss = @options[:iss])
44
+ raise(JWTB::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{@payload['iss'] || '<none>'}") if @payload['iss'].to_s != options_iss.to_s
45
+ end
46
+
47
+ def verify_jti
48
+ options_verify_jti = @options[:verify_jti]
49
+ if options_verify_jti.respond_to?(:call)
50
+ raise(JWTB::InvalidJtiError, 'Invalid jti') unless options_verify_jti.call(@payload['jti'])
51
+ elsif @payload['jti'].to_s.strip.empty?
52
+ raise(JWTB::InvalidJtiError, 'Missing jti')
53
+ end
54
+ end
55
+
56
+ def verify_not_before
57
+ return unless @payload.include?('nbf')
58
+ raise(JWTB::ImmatureSignature, 'Signature nbf has not been reached') if @payload['nbf'].to_i > (Time.now.to_i + nbf_leeway)
59
+ end
60
+
61
+ def verify_sub
62
+ return unless (options_sub = @options[:sub])
63
+ raise(JWTB::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{@payload['sub'] || '<none>'}") unless @payload['sub'].to_s == options_sub.to_s
64
+ end
65
+
66
+ private
67
+
68
+ def global_leeway
69
+ @options[:leeway]
70
+ end
71
+
72
+ def exp_leeway
73
+ @options[:exp_leeway] || global_leeway
74
+ end
75
+
76
+ def iat_leeway
77
+ @options[:iat_leeway] || global_leeway
78
+ end
79
+
80
+ def nbf_leeway
81
+ @options[:nbf_leeway] || global_leeway
82
+ end
83
+ end
84
+ end