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 +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:
|