json_web_token 0.0.2 → 0.1.0
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.
- checksums.yaml +4 -4
- data/.travis.yml +7 -0
- data/CHANGELOG.md +7 -2
- data/README.md +27 -14
- data/lib/json_web_token/algorithm/ecdsa.rb +50 -0
- data/lib/json_web_token/format/asn1.rb +61 -0
- data/lib/json_web_token/jwa.rb +3 -1
- data/lib/json_web_token/version.rb +1 -1
- data/spec/json_web_token/algorithm/ecdsa_spec.rb +56 -0
- data/spec/json_web_token/algorithm/hmac_spec.rb +0 -3
- data/spec/json_web_token/algorithm/rsa_spec.rb +0 -1
- data/spec/json_web_token/format/asn1_spec.rb +105 -0
- data/spec/json_web_token/jwa_spec.rb +36 -38
- data/spec/json_web_token/jws_spec.rb +17 -1
- data/spec/json_web_token/jwt_spec.rb +36 -15
- data/spec/support/ecdsa_key.rb +30 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b8edcad4e223e03c0e62c87821bda85d261108a4
|
4
|
+
data.tar.gz: 3eaa77429d1e1e114226ad5db6ade121a46c56f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e39740af0f48f4781537d5b64ad6f94eaa21945d96155a9beed99db588509201233c07f320eda7aa3ba78126a800c857e35a03c26b1fb4d90935e72061c4aee
|
7
|
+
data.tar.gz: 84d4f1ad8cdb91063fcfa3ad48706722ebae3df3cf78deb947a5c512c62f56c78fada15408b840e5d2014ea24a20f69f4f4d5bc496bfa7e6fd19d4576f009989
|
data/.travis.yml
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
## Changelog
|
2
2
|
|
3
|
-
###
|
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
|
-
###
|
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
|
-
|
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
|
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
|
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
|
-
|
55
|
+
opts = {
|
53
56
|
alg: 'RSA256',
|
54
57
|
key: < RSA private key >
|
55
58
|
}
|
56
59
|
|
57
|
-
jwt = JsonWebToken.create({foo: 'bar'},
|
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
|
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
|
-
* **
|
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
|
-
|
85
|
+
secure_jwt_example = 'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt.cGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
|
81
86
|
|
82
87
|
# verify with default algorithm, HMAC SHA256
|
83
|
-
claims = JsonWebToken.validate(
|
88
|
+
claims = JsonWebToken.validate(secure_jwt_example, key: 'gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr9C')
|
84
89
|
|
85
90
|
# verify with RSA SHA256 algorithm
|
86
|
-
|
91
|
+
opts = {
|
87
92
|
alg: 'RSA256',
|
88
93
|
key: < RSA public key >
|
89
94
|
}
|
90
95
|
|
91
|
-
claims = JsonWebToken.validate(jwt,
|
96
|
+
claims = JsonWebToken.validate(jwt, opts)
|
92
97
|
|
93
98
|
# unsecured token (algorithm is 'none')
|
94
99
|
|
95
|
-
|
100
|
+
unsecured_jwt_example = 'eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt.'
|
96
101
|
|
97
|
-
claims = JsonWebToken.validate(
|
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
|
-
-
|
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
|
data/lib/json_web_token/jwa.rb
CHANGED
@@ -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
|
@@ -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
|
@@ -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
|
-
|
6
|
-
|
7
|
-
|
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(:
|
22
|
-
let(:verifying_key) {
|
23
|
-
it_behaves_like '#
|
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(:
|
29
|
-
let(:verifying_key) {
|
30
|
-
it_behaves_like '#
|
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 '
|
34
|
-
let(:create_options) { {
|
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
|
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
|
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
|
52
|
-
let(:create_options) { {
|
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
|
66
|
-
let(:create_options) { {
|
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) { {
|
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
|
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
|
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
|
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
|
+
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:
|