json_web_token 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d1518a4e0fe92dc8de73a3ed606115ac89ef466b
4
- data.tar.gz: 5d38d88d1307780db38343d0c23d315963fa376d
3
+ metadata.gz: b8edcad4e223e03c0e62c87821bda85d261108a4
4
+ data.tar.gz: 3eaa77429d1e1e114226ad5db6ade121a46c56f8
5
5
  SHA512:
6
- metadata.gz: 781e08e80e4ec08fa00f02e7d7127bca5ba50ce83f2708c7e5135c53b4edeaac53891bac3a0fcc517112511210ab55fd38552e105db62bc631453f3f5df289c1
7
- data.tar.gz: 4c55609063885e415f48ea97c9169076837551fdc2a779f80ee27d14442cdfaf487d3f3dcdeae9fca6ceb6000af210b81d12876e7697250f7ec7ccaff9d976bc
6
+ metadata.gz: 9e39740af0f48f4781537d5b64ad6f94eaa21945d96155a9beed99db588509201233c07f320eda7aa3ba78126a800c857e35a03c26b1fb4d90935e72061c4aee
7
+ data.tar.gz: 84d4f1ad8cdb91063fcfa3ad48706722ebae3df3cf78deb947a5c512c62f56c78fada15408b840e5d2014ea24a20f69f4f4d5bc496bfa7e6fd19d4576f009989
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
4
+ - 2.1.6
5
+ - 2.0.0
6
+ # uncomment this line if your project needs to run something other than `rake`:
7
+ script: bundle exec rspec spec
@@ -1,11 +1,16 @@
1
1
  ## Changelog
2
2
 
3
- ### 0.0.2 2015-07-11
3
+ ### v0.1.0 (2015-07-12)
4
+
5
+ * enhancements
6
+ * support ECDSA algorithm
7
+
8
+ ### v0.0.2 (2015-07-11)
4
9
 
5
10
  * enhancements
6
11
  * support RSASSA-PKCS-v1_5 algorithm
7
12
 
8
- ### 0.0.1 2015-07-09
13
+ ### v0.0.1 (2015-07-09)
9
14
 
10
15
  * initial
11
16
  * support HMAC algorithm
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # JSON Web Token
2
2
 
3
3
  ## A JSON Web Token implementation for Ruby
4
- **Work in progress -- not yet ready for production**
4
+
5
+ [![code_climate][cc_img]][code_climate]
5
6
 
6
7
  ### Description
7
8
  A Ruby implementation of the JSON Web Token (JWT) Standards Track [RFC 7519][rfc7519]
@@ -16,10 +17,12 @@ A Ruby implementation of the JSON Web Token (JWT) Standards Track [RFC 7519][rfc
16
17
  - JSON Web Algorithms (JWA) Standards Track [RFC 7518][rfc7518]
17
18
  * Thorough test coverage
18
19
  * Modularity for comprehension and extensibility
20
+ * Fail fast and hard, with maximally strict validation
21
+ - Inspired by [The Harmful Consequences of Postel's Maxim][thomson-postel]
19
22
  * Implement only the REQUIRED elements of the JWT standard (initially)
20
23
 
21
24
  ### Intended Audience
22
- Token authentication of API requests to Rails via these popular gems
25
+ Token authentication of API requests to Rails via these prominent gems:
23
26
 
24
27
  - [Devise][devise]
25
28
  - [Doorkeeper][doorkeeper]
@@ -37,7 +40,7 @@ Returns a JSON Web Token string
37
40
 
38
41
  `options` (optional) hash
39
42
 
40
- * **alg**, default: `HS256`
43
+ * **alg** (optional, default: `HS256`)
41
44
  * **key** (required unless alg is 'none')
42
45
 
43
46
  Example
@@ -49,12 +52,12 @@ require 'json_web_token'
49
52
  jwt = JsonWebToken.create({foo: 'bar'}, key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C')
50
53
 
51
54
  # sign with RSA SHA256 algorithm
52
- options = {
55
+ opts = {
53
56
  alg: 'RSA256',
54
57
  key: < RSA private key >
55
58
  }
56
59
 
57
- jwt = JsonWebToken.create({foo: 'bar'}, options)
60
+ jwt = JsonWebToken.create({foo: 'bar'}, opts)
58
61
 
59
62
  # unsecured token (algorithm is 'none')
60
63
  jwt = JsonWebToken.create({foo: 'bar'}, alg: 'none')
@@ -63,13 +66,15 @@ jwt = JsonWebToken.create({foo: 'bar'}, alg: 'none')
63
66
 
64
67
  ### JsonWebToken.validate(jwt, options)
65
68
 
66
- Returns a JWT claims set string or hash, if the MAC signature is valid
69
+ Returns either:
70
+ * a JWT claims set string or hash, if the Message Authentication Code (MAC), or signature, is verified
71
+ * a string, 'Invalid', otherwise
67
72
 
68
73
  `jwt` (required) is a JSON web token string
69
74
 
70
75
  `options` (optional) hash
71
76
 
72
- * **algorithm**, default: `HS256`
77
+ * **alg** (optional, default: `HS256`)
73
78
  * **key** (required unless alg is 'none')
74
79
 
75
80
  Example
@@ -77,24 +82,24 @@ Example
77
82
  ```ruby
78
83
  require 'json_web_token'
79
84
 
80
- secure_jwt = 'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt.cGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
85
+ secure_jwt_example = 'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt.cGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
81
86
 
82
87
  # verify with default algorithm, HMAC SHA256
83
- claims = JsonWebToken.validate(secure_jwt, key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C')
88
+ claims = JsonWebToken.validate(secure_jwt_example, key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C')
84
89
 
85
90
  # verify with RSA SHA256 algorithm
86
- options = {
91
+ opts = {
87
92
  alg: 'RSA256',
88
93
  key: < RSA public key >
89
94
  }
90
95
 
91
- claims = JsonWebToken.validate(jwt, options)
96
+ claims = JsonWebToken.validate(jwt, opts)
92
97
 
93
98
  # unsecured token (algorithm is 'none')
94
99
 
95
- unsecured_jwt = 'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt.'
100
+ unsecured_jwt_example = 'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt.'
96
101
 
97
- claims = JsonWebToken.validate(unsecured_jwt, alg: 'none')
102
+ claims = JsonWebToken.validate(unsecured_jwt_example, alg: 'none')
98
103
 
99
104
  ```
100
105
  ### Supported encryption algorithms
@@ -107,6 +112,9 @@ HS512 | HMAC using SHA-512
107
112
  RS256 | RSASSA-PKCS-v1_5 using SHA-256 per [RFC3447][rfc3447]
108
113
  RS384 | RSASSA-PKCS-v1_5 using SHA-384
109
114
  RS512 | RSASSA-PKCS-v1_5 using SHA-512
115
+ ES256 | ECDSA using P-256 and SHA-256 per [DSS][dss]
116
+ ES384 | ECDSA using P-384 and SHA-384
117
+ ES512 | ECDSA using P-521 and SHA-512
110
118
  none | No digital signature or MAC performed (unsecured)
111
119
 
112
120
  ### Supported Ruby Versions
@@ -115,7 +123,7 @@ Ruby 2.0 and up
115
123
  ### Limitations
116
124
  Future implementation may include these features:
117
125
 
118
- - additional RECOMMENDED or OPTIONAL encryption algorithms
126
+ - processing of OPTIONAL JWT registered claim names (e.g. 'exp')
119
127
  - representation of a JWT as a JSON Web Encryption (JWE) [RFC 7516][rfc7516]
120
128
  - OPTIONAL nested JWTs
121
129
 
@@ -125,9 +133,14 @@ Future implementation may include these features:
125
133
  [rfc7516]: http://tools.ietf.org/html/rfc7516
126
134
  [rfc7518]: http://tools.ietf.org/html/rfc7518
127
135
  [rfc7519]: http://tools.ietf.org/html/rfc7519
136
+ [dss]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
128
137
 
138
+ [thomson-postel]: https://tools.ietf.org/html/draft-thomson-postel-was-wrong-00
129
139
  [cors]: http://www.w3.org/TR/cors/
130
140
  [devise]: https://github.com/plataformatec/devise
131
141
  [doorkeeper]: https://github.com/doorkeeper-gem/doorkeeper
132
142
  [oauth2]: https://github.com/intridea/oauth2
133
143
  [rack-cors]: https://github.com/cyu/rack-cors
144
+
145
+ [code_climate]: https://codeclimate.com/github/garyf/json_web_token
146
+ [cc_img]: https://codeclimate.com/github/garyf/json_web_token/badges/gpa.svg
@@ -0,0 +1,50 @@
1
+ require 'json_web_token/algorithm/common'
2
+ require 'json_web_token/format/asn1'
3
+
4
+ module JsonWebToken
5
+ module Algorithm
6
+ module Ecdsa
7
+
8
+ extend JsonWebToken::Algorithm::Common
9
+ extend JsonWebToken::Format::Asn1
10
+
11
+ MAC_BYTE_COUNT = {
12
+ '256' => 64, # secp256k1
13
+ '384' => 96, # secp384r1
14
+ '512' => 132 # secp521r1 note difference (not 128) due to using 521-bit integers
15
+ }
16
+
17
+ module_function
18
+
19
+ def signed(sha_bits, private_key, data)
20
+ validate_key(private_key, sha_bits)
21
+ der = private_key.dsa_sign_asn1(ssl_digest_hash sha_bits, data)
22
+ der_to_signature(der, sha_bits)
23
+ end
24
+
25
+ def verified?(signature, sha_bits, key, data)
26
+ validate_key(key, sha_bits)
27
+ validate_signature_size(signature, sha_bits)
28
+ der = signature_to_der(signature, sha_bits)
29
+ key.dsa_verify_asn1(ssl_digest_hash(sha_bits, data), der)
30
+ end
31
+
32
+ # private
33
+
34
+ def validate_key_size(_key, _sha_bits); end
35
+
36
+ def ssl_digest_hash(sha_bits, data)
37
+ digest_new(sha_bits).digest(data)
38
+ end
39
+
40
+ def validate_signature_size(signature, sha_bits)
41
+ n = MAC_BYTE_COUNT[sha_bits]
42
+ fail('Invalid signature') unless signature && signature.bytesize == n
43
+ end
44
+
45
+ private_class_method :validate_key_size,
46
+ :ssl_digest_hash,
47
+ :validate_signature_size
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,61 @@
1
+ require 'openssl'
2
+
3
+ module JsonWebToken
4
+ module Format
5
+ module Asn1
6
+
7
+ KEY_BITS = {
8
+ '256' => 256,
9
+ '384' => 384,
10
+ '512' => 521 # note difference
11
+ }
12
+
13
+ module_function
14
+
15
+ # ASN1 data structures are usually encoded using the Distinguished Encoding Rules (DER).
16
+ # The ASN1 module provides the necessary classes that allow generation of ASN1 data
17
+ # structures and the methods to encode them using a DER encoding. The decode method allows
18
+ # parsing arbitrary DER-encoded data to a Ruby object that can then be modified and
19
+ # re-encoded at will. (see http://docs.ruby-lang.org/en/2.1.0/OpenSSL/ASN1.html)
20
+
21
+ def der_to_signature(der, sha_bits)
22
+ signature_pair = OpenSSL::ASN1.decode(der).value
23
+ width = per_part_byte_count(sha_bits)
24
+ signature_pair.map { |part| part.value.to_s(2).rjust(width, "\x00") }.join
25
+ end
26
+
27
+ def signature_to_der(signature, sha_bits)
28
+ hsh = destructured_sig(signature, sha_bits)
29
+ asn1_seq = OpenSSL::ASN1::Sequence.new([
30
+ asn1_int(hsh[:r]),
31
+ asn1_int(hsh[:s])
32
+ ])
33
+ asn1_seq.to_der
34
+ end
35
+
36
+ # private
37
+
38
+ def per_part_byte_count(sha_bits)
39
+ bits = KEY_BITS[sha_bits]
40
+ bits ? (bits + 7) / 8 : fail('Invalid sha_bits')
41
+ end
42
+
43
+ def destructured_sig(signature, sha_bits)
44
+ n = per_part_byte_count(sha_bits)
45
+ fail('Invalid signature length') unless signature.length == n * 2
46
+ {
47
+ r: signature[0, n],
48
+ s: signature[n, n]
49
+ }
50
+ end
51
+
52
+ def asn1_int(int)
53
+ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new int, 2)
54
+ end
55
+
56
+ private_class_method :per_part_byte_count,
57
+ :destructured_sig,
58
+ :asn1_int
59
+ end
60
+ end
61
+ end
@@ -1,10 +1,11 @@
1
+ require 'json_web_token/algorithm/ecdsa'
1
2
  require 'json_web_token/algorithm/hmac'
2
3
  require 'json_web_token/algorithm/rsa'
3
4
 
4
5
  module JsonWebToken
5
6
  module Jwa
6
7
 
7
- ALGORITHMS = /(HS|RS)(256|384|512)?/i
8
+ ALGORITHMS = /(HS|RS|ES)(256|384|512)?/i
8
9
  ALG_LENGTH = 5
9
10
 
10
11
  module_function
@@ -39,6 +40,7 @@ module JsonWebToken
39
40
  case str
40
41
  when 'hs' then Algorithm::Hmac
41
42
  when 'rs' then Algorithm::Rsa
43
+ when 'es' then Algorithm::Ecdsa
42
44
  else fail('Unsupported algorithm')
43
45
  end
44
46
  end
@@ -1,3 +1,3 @@
1
1
  module JsonWebToken
2
- VERSION = '0.0.2'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -0,0 +1,56 @@
1
+ require 'json_web_token/algorithm/ecdsa'
2
+ require 'support/ecdsa_key'
3
+
4
+ module JsonWebToken
5
+ module Algorithm
6
+ describe Ecdsa do
7
+ describe 'detect changed signature or data' do
8
+ let(:signing_input_0) { 'signing_input_0' }
9
+ let(:signing_input_1) { 'signing_input_1' }
10
+ shared_examples_for '#signed' do
11
+ it 'is #verified?' do
12
+ private_key_0 = EcdsaKey.curve_new(sha_bits)
13
+ public_key_str_0 = EcdsaKey.public_key_str(private_key_0)
14
+ public_key_0 = EcdsaKey.public_key_new(sha_bits, public_key_str_0)
15
+
16
+ mac_0 = Ecdsa.signed(sha_bits, private_key_0, signing_input_0)
17
+ expect(mac_0.bytes.count).to eql expected_mac_byte_count
18
+ expect(Ecdsa.verified? mac_0, sha_bits, public_key_0, signing_input_0).to be true
19
+
20
+ private_key_1 = EcdsaKey.curve_new(sha_bits)
21
+ public_key_str_1 = EcdsaKey.public_key_str(private_key_1)
22
+ public_key_1 = EcdsaKey.public_key_new(sha_bits, public_key_str_1)
23
+
24
+ expect(Ecdsa.verified? mac_0, sha_bits, public_key_0, signing_input_1).to be false
25
+ expect(Ecdsa.verified? mac_0, sha_bits, public_key_1, signing_input_0).to be false
26
+ expect(Ecdsa.verified? mac_0, sha_bits, public_key_1, signing_input_1).to be false
27
+
28
+ mac_1 = Ecdsa.signed(sha_bits, private_key_1, signing_input_1)
29
+ expect(Ecdsa.verified? mac_1, sha_bits, public_key_0, signing_input_0).to be false
30
+ expect(Ecdsa.verified? mac_1, sha_bits, public_key_0, signing_input_1).to be false
31
+ expect(Ecdsa.verified? mac_1, sha_bits, public_key_1, signing_input_0).to be false
32
+ expect(Ecdsa.verified? mac_1, sha_bits, public_key_1, signing_input_1).to be true
33
+ end
34
+ end
35
+
36
+ describe 'ES256' do
37
+ let(:sha_bits) { '256' }
38
+ let(:expected_mac_byte_count) { 64 }
39
+ it_behaves_like '#signed'
40
+ end
41
+
42
+ describe 'ES384' do
43
+ let(:sha_bits) { '384' }
44
+ let(:expected_mac_byte_count) { 96 }
45
+ it_behaves_like '#signed'
46
+ end
47
+
48
+ describe 'ES512' do
49
+ let(:sha_bits) { '512' }
50
+ let(:expected_mac_byte_count) { 132 }
51
+ it_behaves_like '#signed'
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -78,7 +78,6 @@ module JsonWebToken
78
78
  it 'returns a 32-byte MAC string' do
79
79
  mac = Hmac.signed(sha_bits, key, data)
80
80
  expect(mac.bytesize).to eql 32
81
- expect(mac.class).to eql String
82
81
  end
83
82
  end
84
83
  end
@@ -95,7 +94,6 @@ module JsonWebToken
95
94
  it 'returns a 48-byte MAC string' do
96
95
  mac = Hmac.signed(sha_bits, key, data)
97
96
  expect(mac.bytesize).to eql 48
98
- expect(mac.class).to eql String
99
97
  end
100
98
  end
101
99
  end
@@ -112,7 +110,6 @@ module JsonWebToken
112
110
  it 'returns a 64-byte MAC string' do
113
111
  mac = Hmac.signed(sha_bits, key, data)
114
112
  expect(mac.bytesize).to eql 64
115
- expect(mac.class).to eql String
116
113
  end
117
114
  end
118
115
  end
@@ -75,7 +75,6 @@ module JsonWebToken
75
75
  it 'returns a 256-byte MAC string' do
76
76
  mac = Rsa.signed(sha_bits, private_key, data)
77
77
  expect(mac.bytesize).to eql 256
78
- expect(mac.class).to eql String
79
78
  end
80
79
  end
81
80
 
@@ -0,0 +1,105 @@
1
+ require 'json_web_token/format/asn1'
2
+
3
+ module JsonWebToken
4
+ module Format
5
+ describe Asn1 do
6
+ context 'w bytes' do
7
+ let(:der) { der_bytes.map(&:chr).join }
8
+ let(:signature) { signature_bytes.map(&:chr).join }
9
+ shared_examples_for '#der_to_signature' do
10
+ it 'converts' do
11
+ expect(signature.bytes.length).to eql signature_byte_count
12
+ expect(Asn1.der_to_signature(der, sha_bits).bytes).to eql signature_bytes
13
+ end
14
+ end
15
+
16
+ shared_examples_for '#signature_to_der' do
17
+ it 'converts' do
18
+ expect(Asn1.signature_to_der(signature, sha_bits).bytes).to eql der_bytes
19
+ end
20
+ end
21
+
22
+ shared_examples_for 'w/o valid signature' do
23
+ let(:signature_invalid) { (signature_bytes + [123]).map(&:chr).join }
24
+ it '#signature_to_der raises' do
25
+ expect { Asn1.signature_to_der(signature_invalid, sha_bits) }
26
+ .to raise_error(RuntimeError, 'Invalid signature length')
27
+ end
28
+ end
29
+
30
+ context 'for ES256' do
31
+ let(:sha_bits) { '256' }
32
+ let(:der_bytes) { [48, 69, 2, 32, 39, 115, 251, 5, 254, 60, 42, 53, 128, 68, 123, 82,
33
+ 222, 136, 26, 167, 246, 163, 233, 216, 206, 122, 106, 141, 43, 143, 137, 3, 88, 196,
34
+ 235, 161, 2, 33, 0, 143, 213, 54, 244, 194, 216, 188, 161, 77, 28, 87, 205, 16, 160,
35
+ 11, 125, 21, 62, 206, 233, 242, 201, 149, 152, 53, 25, 103, 6, 4, 56, 193, 161] }
36
+ let(:signature_bytes) { [39, 115, 251, 5, 254, 60, 42, 53, 128, 68, 123, 82, 222, 136,
37
+ 26, 167, 246, 163, 233, 216, 206, 122, 106, 141, 43, 143, 137, 3, 88, 196, 235, 161,
38
+ 143, 213, 54, 244, 194, 216, 188, 161, 77, 28, 87, 205, 16, 160, 11, 125, 21, 62,
39
+ 206, 233, 242, 201, 149, 152, 53, 25, 103, 6, 4, 56, 193, 161] }
40
+ let(:signature_byte_count) { 64 }
41
+ it_behaves_like '#der_to_signature'
42
+ it_behaves_like '#signature_to_der'
43
+ it_behaves_like 'w/o valid signature'
44
+
45
+ describe 'invalid sha_bits' do
46
+ let(:invalid_sha_bits) { '257' }
47
+ it '#der_to_signature raises' do
48
+ expect { Asn1.der_to_signature(der, invalid_sha_bits) }
49
+ .to raise_error(RuntimeError, 'Invalid sha_bits')
50
+ end
51
+
52
+ it '#signature_to_der raises' do
53
+ expect { Asn1.signature_to_der(signature, invalid_sha_bits) }
54
+ .to raise_error(RuntimeError, 'Invalid sha_bits')
55
+ end
56
+ end
57
+ end
58
+
59
+ context 'for ES384' do
60
+ let(:sha_bits) { '384' }
61
+ let(:der_bytes) { [48, 101, 2, 48, 22, 221, 123, 224, 5, 100, 163, 31, 98, 78, 240,
62
+ 249, 85, 126, 120, 130, 228, 123, 69, 2, 21, 65, 249, 229, 151, 208, 186, 162, 31,
63
+ 149, 42, 165, 134, 214, 197, 176, 120, 10, 205, 247, 176, 19, 2, 156, 112, 89, 58,
64
+ 234, 2, 49, 0, 255, 43, 120, 92, 206, 84, 88, 29, 109, 225, 254, 162, 37, 255, 127,
65
+ 231, 37, 178, 36, 173, 225, 201, 121, 154, 43, 122, 229, 114, 50, 83, 69, 243, 143,
66
+ 248, 89, 109, 136, 233, 223, 148, 137, 226, 96, 78, 166, 141, 222, 236] }
67
+ let(:signature_bytes) { [22, 221, 123, 224, 5, 100, 163, 31, 98, 78, 240, 249, 85,
68
+ 126, 120, 130, 228, 123, 69, 2, 21, 65, 249, 229, 151, 208, 186, 162, 31, 149, 42,
69
+ 165, 134, 214, 197, 176, 120, 10, 205, 247, 176, 19, 2, 156, 112, 89, 58, 234, 255,
70
+ 43, 120, 92, 206, 84, 88, 29, 109, 225, 254, 162, 37, 255, 127, 231, 37, 178, 36,
71
+ 173, 225, 201, 121, 154, 43, 122, 229, 114, 50, 83, 69, 243, 143, 248, 89, 109, 136,
72
+ 233, 223, 148, 137, 226, 96, 78, 166, 141, 222, 236] }
73
+ let(:signature_byte_count) { 96 }
74
+ it_behaves_like '#der_to_signature'
75
+ it_behaves_like '#signature_to_der'
76
+ it_behaves_like 'w/o valid signature'
77
+ end
78
+
79
+ context 'for ES512' do
80
+ let(:sha_bits) { '512' }
81
+ let(:der_bytes) { [48, 129, 135, 2, 66, 0, 173, 236, 131, 242, 12, 189, 123, 8, 129,
82
+ 2, 239, 202, 73, 168, 134, 216, 173, 241, 30, 1, 216, 177, 69, 61, 2, 196, 126, 145,
83
+ 132, 172, 174, 210, 133, 191, 50, 57, 239, 229, 201, 118, 197, 62, 197, 62, 128,
84
+ 143, 82, 84, 251, 80, 18, 196, 194, 198, 62, 144, 16, 149, 26, 67, 3, 215, 235, 179,
85
+ 146, 2, 65, 40, 137, 198, 254, 15, 50, 214, 252, 43, 65, 203, 163, 140, 204, 66,
86
+ 159, 53, 125, 184, 29, 24, 189, 249, 21, 64, 109, 87, 100, 165, 139, 83, 129, 190,
87
+ 121, 180, 86, 241, 83, 238, 39, 63, 25, 247, 253, 130, 153, 47, 27, 138, 164, 221,
88
+ 25, 151, 135, 144, 84, 240, 46, 59, 94, 99, 147, 138, 103, 67] }
89
+ let(:signature_bytes) { [0, 173, 236, 131, 242, 12, 189, 123, 8, 129, 2, 239, 202, 73,
90
+ 168, 134, 216, 173, 241, 30, 1, 216, 177, 69, 61, 2, 196, 126, 145, 132, 172, 174,
91
+ 210, 133, 191, 50, 57, 239, 229, 201, 118, 197, 62, 197, 62, 128, 143, 82, 84, 251,
92
+ 80, 18, 196, 194, 198, 62, 144, 16, 149, 26, 67, 3, 215, 235, 179, 146, 0, 40, 137,
93
+ 198, 254, 15, 50, 214, 252, 43, 65, 203, 163, 140, 204, 66, 159, 53, 125, 184, 29,
94
+ 24, 189, 249, 21, 64, 109, 87, 100, 165, 139, 83, 129, 190, 121, 180, 86, 241, 83,
95
+ 238, 39, 63, 25, 247, 253, 130, 153, 47, 27, 138, 164, 221, 25, 151, 135, 144, 84,
96
+ 240, 46, 59, 94, 99, 147, 138, 103, 67] }
97
+ let(:signature_byte_count) { 132 }
98
+ it_behaves_like '#der_to_signature'
99
+ it_behaves_like '#signature_to_der'
100
+ it_behaves_like 'w/o valid signature'
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,33 +1,50 @@
1
1
  require 'json_web_token/jwa'
2
+ require 'support/ecdsa_key'
2
3
 
3
4
  module JsonWebToken
4
5
  describe Jwa do
5
- context 'detect changed signing_input or MAC' do
6
- let(:signing_input) { 'signing_input' }
7
- let(:changed_signing_input) { 'changed_signing_input' }
8
- shared_examples_for '#signed' do
9
- it 'is #verified?' do
10
- mac = Jwa.signed(algorithm, signing_key, signing_input)
11
- expect(Jwa.verified? mac, algorithm, verifying_key, signing_input).to be true
12
- expect(Jwa.verified? mac, algorithm, verifying_key, changed_signing_input).to be false
13
-
14
- changed_mac = Jwa.signed(algorithm, signing_key, changed_signing_input)
15
- expect(Jwa.verified? changed_mac, algorithm, verifying_key, signing_input).to be false
16
- end
6
+ shared_examples_for 'w #verified?' do
7
+ it 'true' do
8
+ expect(Jwa.verified? mac, algorithm, verifying_key, signing_input).to be true
17
9
  end
18
-
10
+ end
11
+ context '#signed' do
12
+ let(:signing_input) { 'signing_input' }
13
+ let(:mac) { Jwa.signed(algorithm, private_key, signing_input) }
19
14
  describe 'HS256' do
20
15
  let(:algorithm) { 'HS256' }
21
- let(:signing_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' }
22
- let(:verifying_key) { signing_key }
23
- it_behaves_like '#signed'
16
+ let(:private_key) { 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C' }
17
+ let(:verifying_key) { private_key }
18
+ it_behaves_like 'w #verified?'
19
+
20
+ it 'returns a 32-byte MAC' do
21
+ expect(mac.bytesize).to eql 32
22
+ end
24
23
  end
25
24
 
26
25
  describe 'RS256' do
27
26
  let(:algorithm) { 'RS256' }
28
- let(:signing_key) { OpenSSL::PKey::RSA.generate(2048) }
29
- let(:verifying_key) { signing_key.public_key }
30
- it_behaves_like '#signed'
27
+ let(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
28
+ let(:verifying_key) { private_key.public_key }
29
+ it_behaves_like 'w #verified?'
30
+
31
+ it 'returns a 256-byte MAC' do
32
+ expect(mac.bytesize).to eql 256
33
+ end
34
+ end
35
+
36
+ describe 'ES256' do
37
+ let(:algorithm) { 'ES256' }
38
+ it 'w #verified? true, returns a 64-byte MAC' do
39
+ private_key = EcdsaKey.curve_new('256')
40
+ public_key_str = EcdsaKey.public_key_str(private_key)
41
+ public_key = EcdsaKey.public_key_new('256', public_key_str)
42
+
43
+ mac = Jwa.signed(algorithm, private_key, signing_input)
44
+ expect(Jwa.verified? mac, algorithm, public_key, signing_input).to be true
45
+
46
+ expect(mac.bytesize).to eql 64
47
+ end
31
48
  end
32
49
  end
33
50
 
@@ -44,25 +61,6 @@ module JsonWebToken
44
61
  end
45
62
  end
46
63
  end
47
-
48
- describe 'HS256' do
49
- let(:algorithm) { 'HS256' }
50
- it 'returns a 32-byte MAC string' do
51
- mac = Jwa.signed(algorithm, key, data)
52
- expect(mac.bytesize).to eql 32
53
- expect(mac.class).to eql String
54
- end
55
- end
56
- end
57
-
58
- describe 'RS256' do
59
- let(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
60
- let(:algorithm) { 'RS256' }
61
- it 'returns a 256-byte MAC string' do
62
- mac = Jwa.signed(algorithm, private_key, data)
63
- expect(mac.bytesize).to eql 256
64
- expect(mac.class).to eql String
65
- end
66
64
  end
67
65
  end
68
66
  end
@@ -1,4 +1,5 @@
1
1
  require 'json_web_token/jws'
2
+ require 'support/ecdsa_key'
2
3
 
3
4
  module JsonWebToken
4
5
  describe Jws do
@@ -6,7 +7,7 @@ module JsonWebToken
6
7
  let(:payload) { 'payload' }
7
8
  context '#message_signature' do
8
9
  shared_examples_for 'w #validate' do
9
- it 'verified' do
10
+ it 'is verified' do
10
11
  jws = Jws.message_signature(header, payload, signing_key)
11
12
  expect(Jws.validate jws, algorithm, verifying_key).to eql jws
12
13
  end
@@ -51,6 +52,21 @@ module JsonWebToken
51
52
  end
52
53
  end
53
54
  end
55
+
56
+ context "w ES256 'alg' header parameter" do
57
+ let(:header) { {alg: 'ES256'} }
58
+ describe 'w passing a matching algorithm to #validate' do
59
+ let(:algorithm) { 'ES256' }
60
+ it 'is verified' do
61
+ private_key = EcdsaKey.curve_new('256')
62
+ public_key_str = EcdsaKey.public_key_str(private_key)
63
+ public_key = EcdsaKey.public_key_new('256', public_key_str)
64
+
65
+ jws = Jws.message_signature(header, payload, private_key)
66
+ expect(Jws.validate jws, algorithm, public_key).to eql jws
67
+ end
68
+ end
69
+ end
54
70
  end
55
71
 
56
72
  context 'header validation' do
@@ -1,11 +1,12 @@
1
1
  require 'json_web_token/jwt'
2
+ require 'support/ecdsa_key'
2
3
  require 'support/plausible_jwt'
3
4
 
4
5
  module JsonWebToken
5
6
  describe Jwt do
6
7
  context '#create' do
7
8
  shared_examples_for 'w #validate' do
8
- it 'verified' do
9
+ it 'is verified' do
9
10
  jwt = Jwt.create(claims, create_options)
10
11
  expect(Jwt.validate jwt, validate_options).to include(claims)
11
12
  end
@@ -30,29 +31,29 @@ module JsonWebToken
30
31
  it_behaves_like 'return message signature'
31
32
  end
32
33
 
33
- describe 'passing header parameters' do
34
- let(:create_options) { {typ: 'JWT', alg: 'HS256', key: signing_key} }
34
+ describe 'w alg option' do
35
+ let(:create_options) { {alg: 'HS256', key: signing_key} }
35
36
  it_behaves_like 'w #validate'
36
37
  it_behaves_like 'return message signature'
37
38
  end
38
39
 
39
- describe "w 'alg':'nil' header parameter" do
40
+ describe 'w alg: nil option' do
40
41
  let(:create_options) { {alg: nil, key: signing_key} }
41
42
  it_behaves_like 'w #validate'
42
43
  it_behaves_like 'return message signature'
43
44
  end
44
45
 
45
- describe "w 'alg':'' header parameter" do
46
+ describe "w alg empty string option" do
46
47
  let(:create_options) { {alg: '', key: signing_key} }
47
48
  it_behaves_like 'w #validate'
48
49
  it_behaves_like 'return message signature'
49
50
  end
50
51
 
51
- describe "w 'alg':'none' header parameter" do
52
- let(:create_options) { {typ: 'JWT', alg: 'none', key: signing_key} }
52
+ describe "w alg: 'none' option" do
53
+ let(:create_options) { {alg: 'none', key: signing_key} }
53
54
  it 'raises' do
54
55
  jwt = Jwt.create(claims, create_options)
55
- expect { Jwt.validate(jwt) }
56
+ expect { Jwt.validate(jwt, validate_options) }
56
57
  .to raise_error(RuntimeError, "Algorithm not matching 'alg' header parameter")
57
58
  end
58
59
  end
@@ -62,9 +63,10 @@ module JsonWebToken
62
63
  let(:signing_key) { OpenSSL::PKey::RSA.generate(2048) }
63
64
  let(:verifying_key) { signing_key.public_key }
64
65
  let(:validate_options) { {alg: 'RS256', key: verifying_key} }
65
- describe 'passing header parameters' do
66
- let(:create_options) { {typ: 'JWT', alg: 'RS256', key: signing_key} }
66
+ describe 'passing matching options' do
67
+ let(:create_options) { {alg: 'RS256', key: signing_key} }
67
68
  it_behaves_like 'w #validate'
69
+
68
70
  it 'plausible' do
69
71
  jwt = Jwt.create(claims, create_options)
70
72
  expect(plausible_message_signature? jwt, 256).to be true
@@ -72,10 +74,29 @@ module JsonWebToken
72
74
  end
73
75
  end
74
76
 
77
+ context "w ES256 'alg' header parameter" do
78
+ let(:algorithm) { 'ES256' }
79
+ describe 'w passing a matching algorithm to #validate' do
80
+ it 'is verified and plausible' do
81
+ private_key = EcdsaKey.curve_new('256')
82
+ public_key_str = EcdsaKey.public_key_str(private_key)
83
+ public_key = EcdsaKey.public_key_new('256', public_key_str)
84
+
85
+ create_options = {alg: algorithm, key: private_key}
86
+ jwt = Jwt.create(claims, create_options)
87
+
88
+ validate_options = {alg: algorithm, key: public_key}
89
+ expect(Jwt.validate jwt, validate_options).to include(claims)
90
+
91
+ expect(plausible_message_signature? jwt, 64).to be true
92
+ end
93
+ end
94
+ end
95
+
75
96
  context 'w/o key' do
76
- context "w alg 'none' header parameter" do
77
- let(:create_options) { {typ: 'JWT', alg: 'none'} }
78
- describe "w validate alg 'none'" do
97
+ context "w alg: 'none' header parameter" do
98
+ let(:create_options) { {alg: 'none'} }
99
+ describe "w validate alg: 'none'" do
79
100
  let(:validate_options) { {alg: 'none'} }
80
101
  it 'validates a plausible unsecured jws' do
81
102
  jwt = Jwt.create(claims, create_options)
@@ -84,7 +105,7 @@ module JsonWebToken
84
105
  end
85
106
  end
86
107
 
87
- describe "w default validate alg" do
108
+ describe 'w default validate alg' do
88
109
  it 'raises' do
89
110
  jwt = Jwt.create(claims, create_options)
90
111
  expect { Jwt.validate(jwt) }
@@ -109,7 +130,7 @@ module JsonWebToken
109
130
  it_behaves_like 'w/o claims'
110
131
  end
111
132
 
112
- describe "w claims ''" do
133
+ describe 'w claims an empty string' do
113
134
  let(:claims) { '' }
114
135
  it_behaves_like 'w/o claims'
115
136
  end
@@ -0,0 +1,30 @@
1
+ require 'openssl'
2
+
3
+ module EcdsaKey
4
+
5
+ BUILT_IN_CURVES = {
6
+ '256' => 'secp256k1',
7
+ '384' => 'secp384r1',
8
+ '512' => 'secp521r1'
9
+ }
10
+
11
+ module_function
12
+
13
+ def curve_new(sha_bits)
14
+ OpenSSL::PKey::EC.new(BUILT_IN_CURVES[sha_bits])
15
+ end
16
+
17
+ def public_key_str(curve, base = 16)
18
+ curve.generate_key unless curve.private_key
19
+ curve.public_key.to_bn.to_s(base)
20
+ end
21
+
22
+ def public_key_new(sha_bits, public_key_str, base = 16)
23
+ curve_name = BUILT_IN_CURVES[sha_bits]
24
+ fail('Unsupported curve') unless curve_name
25
+ group = OpenSSL::PKey::EC::Group.new(curve_name)
26
+ curve = OpenSSL::PKey::EC.new(group)
27
+ curve.public_key = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(public_key_str, base))
28
+ curve
29
+ end
30
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_web_token
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gary Fleshman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-11 00:00:00.000000000 Z
11
+ date: 2015-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -52,6 +52,7 @@ extra_rdoc_files: []
52
52
  files:
53
53
  - ".gitignore"
54
54
  - ".rspec"
55
+ - ".travis.yml"
55
56
  - CHANGELOG.md
56
57
  - Gemfile
57
58
  - LICENSE
@@ -59,16 +60,20 @@ files:
59
60
  - json_web_token.gemspec
60
61
  - lib/json_web_token.rb
61
62
  - lib/json_web_token/algorithm/common.rb
63
+ - lib/json_web_token/algorithm/ecdsa.rb
62
64
  - lib/json_web_token/algorithm/hmac.rb
63
65
  - lib/json_web_token/algorithm/rsa.rb
66
+ - lib/json_web_token/format/asn1.rb
64
67
  - lib/json_web_token/format/base64_url.rb
65
68
  - lib/json_web_token/jwa.rb
66
69
  - lib/json_web_token/jws.rb
67
70
  - lib/json_web_token/jwt.rb
68
71
  - lib/json_web_token/util.rb
69
72
  - lib/json_web_token/version.rb
73
+ - spec/json_web_token/algorithm/ecdsa_spec.rb
70
74
  - spec/json_web_token/algorithm/hmac_spec.rb
71
75
  - spec/json_web_token/algorithm/rsa_spec.rb
76
+ - spec/json_web_token/format/asn1_spec.rb
72
77
  - spec/json_web_token/format/base64_url_spec.rb
73
78
  - spec/json_web_token/jwa_spec.rb
74
79
  - spec/json_web_token/jws_spec.rb
@@ -76,6 +81,7 @@ files:
76
81
  - spec/json_web_token/util_spec.rb
77
82
  - spec/json_web_token_spec.rb
78
83
  - spec/spec_helper.rb
84
+ - spec/support/ecdsa_key.rb
79
85
  - spec/support/plausible_jwt.rb
80
86
  homepage: https://github.com/garyf/json_web_token
81
87
  licenses: