jwtb 2.0.0.beta2.bsk1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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