jose 1.1.1 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/docs/SignatureAlgorithms.md +1 -0
- data/lib/jose/jwa.rb +2 -0
- data/lib/jose/jwe/zip_def.rb +11 -3
- data/lib/jose/jwk/kty_okp_ed25519.rb +6 -6
- data/lib/jose/jwk/kty_okp_ed25519ph.rb +4 -4
- data/lib/jose/jwk/kty_okp_ed448.rb +6 -6
- data/lib/jose/jwk/kty_okp_ed448ph.rb +6 -6
- data/lib/jose/jwk/kty_rsa.rb +65 -1
- data/lib/jose/jws.rb +20 -1
- data/lib/jose/jws/alg_eddsa.rb +4 -1
- data/lib/jose/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa018b696c9f264acc7691bb2ae92a7e48ec95e7
|
4
|
+
data.tar.gz: c07468825b96248e7e15376711e573913f0d33ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41a28230b46f5e9ea6ef56c7c8bdc2f353f7472f18faea6923d39326eb8d87f77a3ae247d22181ef5a4babe550cab1db31361e657d066959e8629a6df87c8e6d
|
7
|
+
data.tar.gz: 00c55722c8dc85d65c53a7628abdd9baa4c536d409249dd5de4487a7b3d8e34254035151526cbedf520809401153604f7c301705274d70d02fbc05fe1665a4e2
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 1.1.2 (2016-07-07)
|
4
|
+
|
5
|
+
* Enhancements
|
6
|
+
* Improved handling of RSA private keys in SMF (Straightforward Method) form to CRT (Chinese Remainder Theorem) form, see [potatosalad/erlang-jose#19](https://github.com/potatosalad/erlang-jose/issues/19) This is especially useful for keys produced by Java programs using the `RSAPrivateKeySpec` API as mentioned in [Section 9.3 of RFC 7517](https://tools.ietf.org/html/rfc7517#section-9.3).
|
7
|
+
* Updated EdDSA operations to comply with draft 04 of [draft-ietf-jose-cfrg-curves-04](https://tools.ietf.org/html/draft-ietf-jose-cfrg-curves-04).
|
8
|
+
|
9
|
+
* Fixes
|
10
|
+
* Fixed compression encoding bug for `{"zip":"DEF"}` operations (thanks to [@amadden734](https://github.com/amadden734) see [#3](https://github.com/potatosalad/ruby-jose/pull/3))
|
11
|
+
|
3
12
|
## 1.1.1 (2016-05-27)
|
4
13
|
|
5
14
|
* Enhancements
|
data/docs/SignatureAlgorithms.md
CHANGED
@@ -21,6 +21,7 @@ Here are the supported options for the `alg` parameter, grouped by similar funci
|
|
21
21
|
- [`Ed25519ph`](http://www.rubydoc.info/gems/jose/JOSE/JWS#EdDSA-25519-group)
|
22
22
|
- [`Ed448`](http://www.rubydoc.info/gems/jose/JOSE/JWS#EdDSA-448-group)
|
23
23
|
- [`Ed448ph`](http://www.rubydoc.info/gems/jose/JOSE/JWS#EdDSA-448-group)
|
24
|
+
- [`EdDSA`](http://www.rubydoc.info/gems/jose/JOSE/JWS#EdDSA-group)
|
24
25
|
- HMAC using SHA-2
|
25
26
|
- [`HS256`](http://www.rubydoc.info/gems/jose/JOSE/JWS#HMACSHA2-group)
|
26
27
|
- [`HS384`](http://www.rubydoc.info/gems/jose/JOSE/JWS#HMACSHA2-group)
|
data/lib/jose/jwa.rb
CHANGED
@@ -104,6 +104,7 @@ module JOSE
|
|
104
104
|
# # "Ed25519ph",
|
105
105
|
# # "Ed448",
|
106
106
|
# # "Ed448ph",
|
107
|
+
# # "EdDSA",
|
107
108
|
# # "ES256",
|
108
109
|
# # "ES384",
|
109
110
|
# # "ES512",
|
@@ -161,6 +162,7 @@ module JOSE
|
|
161
162
|
'Ed25519ph',
|
162
163
|
'Ed448',
|
163
164
|
'Ed448ph',
|
165
|
+
'EdDSA',
|
164
166
|
'ES256',
|
165
167
|
'ES384',
|
166
168
|
'ES512',
|
data/lib/jose/jwe/zip_def.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
class JOSE::JWE::ZIP_DEF
|
1
|
+
class JOSE::JWE::ZIP_DEF < Struct.new(nil)
|
2
2
|
|
3
3
|
# JOSE::JWE callbacks
|
4
4
|
|
@@ -18,11 +18,19 @@ class JOSE::JWE::ZIP_DEF
|
|
18
18
|
# JOSE::JWE::ZIP callbacks
|
19
19
|
|
20
20
|
def compress(plain_text)
|
21
|
-
|
21
|
+
zstream = Zlib::Deflate.new(nil, -Zlib::MAX_WBITS)
|
22
|
+
buf = zstream.deflate(plain_text, Zlib::FINISH)
|
23
|
+
zstream.finish
|
24
|
+
zstream.close
|
25
|
+
return buf
|
22
26
|
end
|
23
27
|
|
24
28
|
def uncompress(cipher_text)
|
25
|
-
|
29
|
+
zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
30
|
+
buf = zstream.inflate(cipher_text)
|
31
|
+
zstream.finish
|
32
|
+
zstream.close
|
33
|
+
return buf
|
26
34
|
end
|
27
35
|
|
28
36
|
end
|
@@ -78,8 +78,8 @@ class JOSE::JWK::KTY_OKP_Ed25519 < Struct.new(:okp)
|
|
78
78
|
return JOSE::JWK::KTY.key_encryptor(self, fields, key)
|
79
79
|
end
|
80
80
|
|
81
|
-
def sign(message,
|
82
|
-
raise ArgumentError, "'
|
81
|
+
def sign(message, sign_type)
|
82
|
+
raise ArgumentError, "'sign_type' must be :Ed25519 or :EdDSA" if sign_type != :Ed25519 and sign_type != :EdDSA
|
83
83
|
raise NotImplementedError, "Ed25519 public key cannot be used for signing" if okp.bytesize != SK_BYTES
|
84
84
|
return JOSE::JWA::Curve25519.ed25519_sign(message, okp)
|
85
85
|
end
|
@@ -88,7 +88,7 @@ class JOSE::JWK::KTY_OKP_Ed25519 < Struct.new(:okp)
|
|
88
88
|
if okp.bytesize == SK_BYTES and fields and fields['use'] == 'sig' and not fields['alg'].nil?
|
89
89
|
return JOSE::Map['alg' => fields['alg']]
|
90
90
|
elsif okp.bytesize == SK_BYTES
|
91
|
-
return JOSE::Map['alg' => '
|
91
|
+
return JOSE::Map['alg' => 'EdDSA']
|
92
92
|
else
|
93
93
|
raise ArgumentError, "signing not supported for public keys"
|
94
94
|
end
|
@@ -98,12 +98,12 @@ class JOSE::JWK::KTY_OKP_Ed25519 < Struct.new(:okp)
|
|
98
98
|
if fields and fields['use'] == 'sig' and not fields['alg'].nil?
|
99
99
|
return [fields['alg']]
|
100
100
|
else
|
101
|
-
return ['Ed25519']
|
101
|
+
return ['Ed25519', 'EdDSA']
|
102
102
|
end
|
103
103
|
end
|
104
104
|
|
105
|
-
def verify(message,
|
106
|
-
raise ArgumentError, "'
|
105
|
+
def verify(message, sign_type, signature)
|
106
|
+
raise ArgumentError, "'sign_type' must be :Ed25519 or :EdDSA" if sign_type != :Ed25519 and sign_type != :EdDSA
|
107
107
|
pk = okp
|
108
108
|
pk = JOSE::JWA::Curve25519.ed25519_secret_to_public(okp) if okp.bytesize == SK_BYTES
|
109
109
|
return JOSE::JWA::Curve25519.ed25519_verify(signature, message, pk)
|
@@ -79,7 +79,7 @@ class JOSE::JWK::KTY_OKP_Ed25519ph < Struct.new(:okp)
|
|
79
79
|
end
|
80
80
|
|
81
81
|
def sign(message, sign_type)
|
82
|
-
raise ArgumentError, "'sign_type' must be :Ed25519ph" if sign_type != :Ed25519ph
|
82
|
+
raise ArgumentError, "'sign_type' must be :Ed25519ph or :EdDSA" if sign_type != :Ed25519ph and sign_type != :EdDSA
|
83
83
|
raise NotImplementedError, "Ed25519ph public key cannot be used for signing" if okp.bytesize != SK_BYTES
|
84
84
|
return JOSE::JWA::Curve25519.ed25519ph_sign(message, okp)
|
85
85
|
end
|
@@ -88,7 +88,7 @@ class JOSE::JWK::KTY_OKP_Ed25519ph < Struct.new(:okp)
|
|
88
88
|
if okp.bytesize == SK_BYTES and fields and fields['use'] == 'sig' and not fields['alg'].nil?
|
89
89
|
return JOSE::Map['alg' => fields['alg']]
|
90
90
|
elsif okp.bytesize == SK_BYTES
|
91
|
-
return JOSE::Map['alg' => '
|
91
|
+
return JOSE::Map['alg' => 'EdDSA']
|
92
92
|
else
|
93
93
|
raise ArgumentError, "signing not supported for public keys"
|
94
94
|
end
|
@@ -98,12 +98,12 @@ class JOSE::JWK::KTY_OKP_Ed25519ph < Struct.new(:okp)
|
|
98
98
|
if fields and fields['use'] == 'sig' and not fields['alg'].nil?
|
99
99
|
return [fields['alg']]
|
100
100
|
else
|
101
|
-
return ['Ed25519ph']
|
101
|
+
return ['Ed25519ph', 'EdDSA']
|
102
102
|
end
|
103
103
|
end
|
104
104
|
|
105
105
|
def verify(message, sign_type, signature)
|
106
|
-
raise ArgumentError, "'sign_type' must be :Ed25519ph" if sign_type != :Ed25519ph
|
106
|
+
raise ArgumentError, "'sign_type' must be :Ed25519ph or :EdDSA" if sign_type != :Ed25519ph and sign_type != :EdDSA
|
107
107
|
pk = okp
|
108
108
|
pk = JOSE::JWA::Curve25519.ed25519ph_secret_to_public(okp) if okp.bytesize == SK_BYTES
|
109
109
|
return JOSE::JWA::Curve25519.ed25519ph_verify(signature, message, pk)
|
@@ -78,8 +78,8 @@ class JOSE::JWK::KTY_OKP_Ed448 < Struct.new(:okp)
|
|
78
78
|
return JOSE::JWK::KTY.key_encryptor(self, fields, key)
|
79
79
|
end
|
80
80
|
|
81
|
-
def sign(message,
|
82
|
-
raise ArgumentError, "'
|
81
|
+
def sign(message, sign_type)
|
82
|
+
raise ArgumentError, "'sign_type' must be :Ed448 or :EdDSA" if sign_type != :Ed448 and sign_type != :EdDSA
|
83
83
|
raise NotImplementedError, "Ed448 public key cannot be used for signing" if okp.bytesize != SK_BYTES
|
84
84
|
return JOSE::JWA::Curve448.ed448_sign(message, okp)
|
85
85
|
end
|
@@ -88,7 +88,7 @@ class JOSE::JWK::KTY_OKP_Ed448 < Struct.new(:okp)
|
|
88
88
|
if okp.bytesize == SK_BYTES and fields and fields['use'] == 'sig' and not fields['alg'].nil?
|
89
89
|
return JOSE::Map['alg' => fields['alg']]
|
90
90
|
elsif okp.bytesize == SK_BYTES
|
91
|
-
return JOSE::Map['alg' => '
|
91
|
+
return JOSE::Map['alg' => 'EdDSA']
|
92
92
|
else
|
93
93
|
raise ArgumentError, "signing not supported for public keys"
|
94
94
|
end
|
@@ -98,12 +98,12 @@ class JOSE::JWK::KTY_OKP_Ed448 < Struct.new(:okp)
|
|
98
98
|
if fields and fields['use'] == 'sig' and not fields['alg'].nil?
|
99
99
|
return [fields['alg']]
|
100
100
|
else
|
101
|
-
return ['Ed448']
|
101
|
+
return ['Ed448', 'EdDSA']
|
102
102
|
end
|
103
103
|
end
|
104
104
|
|
105
|
-
def verify(message,
|
106
|
-
raise ArgumentError, "'
|
105
|
+
def verify(message, sign_type, signature)
|
106
|
+
raise ArgumentError, "'sign_type' must be :Ed448 or :EdDSA" if sign_type != :Ed448 and sign_type != :EdDSA
|
107
107
|
pk = okp
|
108
108
|
pk = JOSE::JWA::Curve448.ed448_secret_to_public(okp) if okp.bytesize == SK_BYTES
|
109
109
|
return JOSE::JWA::Curve448.ed448_verify(signature, message, pk)
|
@@ -78,8 +78,8 @@ class JOSE::JWK::KTY_OKP_Ed448ph < Struct.new(:okp)
|
|
78
78
|
return JOSE::JWK::KTY.key_encryptor(self, fields, key)
|
79
79
|
end
|
80
80
|
|
81
|
-
def sign(message,
|
82
|
-
raise ArgumentError, "'
|
81
|
+
def sign(message, sign_type)
|
82
|
+
raise ArgumentError, "'sign_type' must be :Ed448ph or :EdDSA" if sign_type != :Ed448ph and sign_type != :EdDSA
|
83
83
|
raise NotImplementedError, "Ed448ph public key cannot be used for signing" if okp.bytesize != SK_BYTES
|
84
84
|
return JOSE::JWA::Curve448.ed448ph_sign(message, okp)
|
85
85
|
end
|
@@ -88,7 +88,7 @@ class JOSE::JWK::KTY_OKP_Ed448ph < Struct.new(:okp)
|
|
88
88
|
if okp.bytesize == SK_BYTES and fields and fields['use'] == 'sig' and not fields['alg'].nil?
|
89
89
|
return JOSE::Map['alg' => fields['alg']]
|
90
90
|
elsif okp.bytesize == SK_BYTES
|
91
|
-
return JOSE::Map['alg' => '
|
91
|
+
return JOSE::Map['alg' => 'EdDSA']
|
92
92
|
else
|
93
93
|
raise ArgumentError, "signing not supported for public keys"
|
94
94
|
end
|
@@ -98,12 +98,12 @@ class JOSE::JWK::KTY_OKP_Ed448ph < Struct.new(:okp)
|
|
98
98
|
if fields and fields['use'] == 'sig' and not fields['alg'].nil?
|
99
99
|
return [fields['alg']]
|
100
100
|
else
|
101
|
-
return ['Ed448ph']
|
101
|
+
return ['Ed448ph', 'EdDSA']
|
102
102
|
end
|
103
103
|
end
|
104
104
|
|
105
|
-
def verify(message,
|
106
|
-
raise ArgumentError, "'
|
105
|
+
def verify(message, sign_type, signature)
|
106
|
+
raise ArgumentError, "'sign_type' must be :Ed448ph or :EdDSA" if sign_type != :Ed448ph and sign_type != :EdDSA
|
107
107
|
pk = okp
|
108
108
|
pk = JOSE::JWA::Curve448.ed448ph_secret_to_public(okp) if okp.bytesize == SK_BYTES
|
109
109
|
return JOSE::JWA::Curve448.ed448ph_verify(signature, message, pk)
|
data/lib/jose/jwk/kty_rsa.rb
CHANGED
@@ -19,7 +19,11 @@ class JOSE::JWK::KTY_RSA < Struct.new(:key)
|
|
19
19
|
rsa.iqmp = OpenSSL::BN.new(JOSE.urlsafe_decode64(fields['qi']), 2)
|
20
20
|
return JOSE::JWK::KTY_RSA.new(JOSE::JWK::PKeyProxy.new(rsa)), fields.except('kty', 'd', 'dp', 'dq', 'e', 'n', 'p', 'q', 'qi')
|
21
21
|
else
|
22
|
-
|
22
|
+
d = OpenSSL::BN.new(JOSE.urlsafe_decode64(fields['d']), 2)
|
23
|
+
e = OpenSSL::BN.new(JOSE.urlsafe_decode64(fields['e']), 2)
|
24
|
+
n = OpenSSL::BN.new(JOSE.urlsafe_decode64(fields['n']), 2)
|
25
|
+
rsa = convert_sfm_to_crt(d, e, n)
|
26
|
+
return JOSE::JWK::KTY_RSA.new(JOSE::JWK::PKeyProxy.new(rsa)), fields.except('kty', 'd', 'dp', 'dq', 'e', 'n', 'p', 'q', 'qi')
|
23
27
|
end
|
24
28
|
else
|
25
29
|
rsa = OpenSSL::PKey::RSA.new
|
@@ -200,4 +204,64 @@ class JOSE::JWK::KTY_RSA < Struct.new(:key)
|
|
200
204
|
return JOSE::JWK::PEM.to_binary(key, password)
|
201
205
|
end
|
202
206
|
|
207
|
+
# Internal functions
|
208
|
+
|
209
|
+
private
|
210
|
+
def self.convert_sfm_to_crt(d, e, n)
|
211
|
+
ktot = d * e - 1
|
212
|
+
t = convert_sfm_to_crt_find_t(ktot)
|
213
|
+
a = 2.to_bn
|
214
|
+
k = t
|
215
|
+
p = convert_sfm_to_crt_find_p(ktot, t, k, a, n, d)
|
216
|
+
q, = n / p
|
217
|
+
if q > p
|
218
|
+
p, q = q, p
|
219
|
+
end
|
220
|
+
dp = d % (p - 1)
|
221
|
+
dq = d % (q - 1)
|
222
|
+
qi = q.mod_inverse(p)
|
223
|
+
rsa = OpenSSL::PKey::RSA.new
|
224
|
+
rsa.d = d
|
225
|
+
rsa.dmp1 = dp
|
226
|
+
rsa.dmq1 = dq
|
227
|
+
rsa.e = e
|
228
|
+
rsa.n = n
|
229
|
+
rsa.p = p
|
230
|
+
rsa.q = q
|
231
|
+
rsa.iqmp = qi
|
232
|
+
return rsa
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.convert_sfm_to_crt_find_t(ktot)
|
236
|
+
t = ktot
|
237
|
+
loop do
|
238
|
+
if t > 0 and (t % 2) == 0
|
239
|
+
t, = t / 2
|
240
|
+
else
|
241
|
+
break
|
242
|
+
end
|
243
|
+
end
|
244
|
+
return t
|
245
|
+
end
|
246
|
+
|
247
|
+
def self.convert_sfm_to_crt_find_p(ktot, t, k, a, n, d)
|
248
|
+
p = nil
|
249
|
+
loop do
|
250
|
+
break if not p.nil?
|
251
|
+
loop do
|
252
|
+
break if not p.nil?
|
253
|
+
break if k >= ktot
|
254
|
+
c = a.mod_exp(k, n)
|
255
|
+
if c != 1 and c != (n - 1) and c.mod_exp(2, n) == 1
|
256
|
+
p = (c + 1).gcd(n)
|
257
|
+
else
|
258
|
+
k = k * 2
|
259
|
+
end
|
260
|
+
end
|
261
|
+
break if not p.nil?
|
262
|
+
k = t
|
263
|
+
a = a + 2
|
264
|
+
end
|
265
|
+
return p
|
266
|
+
end
|
203
267
|
end
|
data/lib/jose/jws.rb
CHANGED
@@ -59,6 +59,7 @@ module JOSE
|
|
59
59
|
# * `"Ed25519ph"`
|
60
60
|
# * `"Ed448"`
|
61
61
|
# * `"Ed448ph"`
|
62
|
+
# * `"EdDSA"`
|
62
63
|
# * `"ES256"`
|
63
64
|
# * `"ES384"`
|
64
65
|
# * `"ES512"`
|
@@ -115,6 +116,18 @@ module JOSE
|
|
115
116
|
# JOSE::JWS.verify(jwk_ed448ph, signed_ed448ph).first
|
116
117
|
# # => true
|
117
118
|
#
|
119
|
+
# ### <a name="EdDSA-group">EdDSA</a>
|
120
|
+
#
|
121
|
+
# # EdDSA works with Ed25519, Ed25519ph, Ed448, and Ed448ph keys.
|
122
|
+
# # However, it defaults to Ed25519 for key generation.
|
123
|
+
# jwk_eddsa = JOSE::JWS.generate_key({ "alg" => "EdDSA" })
|
124
|
+
#
|
125
|
+
# # EdDSA
|
126
|
+
# signed_eddsa = JOSE::JWS.sign(jwk_eddsa, "{}", { "alg" => "EdDSA" }).compact
|
127
|
+
# # => "eyJhbGciOiJFZERTQSJ9.e30.rhb5ZY7MllNbW9q-SCn_NglhYtaRGMXEUDj6BvJjltOt19tEI_1wFrVK__jL91i9hO7WtVqRH_OfHiilnO1CAQ"
|
128
|
+
# JOSE::JWS.verify(jwk_eddsa, signed_eddsa).first
|
129
|
+
# # => true
|
130
|
+
#
|
118
131
|
# ### <a name="ECDSA-group">ES256, ES384, and ES512</a>
|
119
132
|
#
|
120
133
|
# !!!ruby
|
@@ -681,7 +694,13 @@ module JOSE
|
|
681
694
|
|
682
695
|
private
|
683
696
|
|
684
|
-
EDDSA_ALG_LIST = [
|
697
|
+
EDDSA_ALG_LIST = [
|
698
|
+
'Ed25519'.freeze,
|
699
|
+
'Ed25519ph'.freeze,
|
700
|
+
'Ed448'.freeze,
|
701
|
+
'Ed448ph'.freeze,
|
702
|
+
'EdDSA'.freeze
|
703
|
+
].freeze
|
685
704
|
|
686
705
|
def self.from_fields(jws, modules)
|
687
706
|
if jws.fields.has_key?('b64')
|
data/lib/jose/jws/alg_eddsa.rb
CHANGED
@@ -12,6 +12,8 @@ class JOSE::JWS::ALG_EDDSA < Struct.new(:sign_type)
|
|
12
12
|
return new(:Ed448), fields.delete('alg')
|
13
13
|
when 'Ed448ph'
|
14
14
|
return new(:Ed448ph), fields.delete('alg')
|
15
|
+
when 'EdDSA'
|
16
|
+
return new(:EdDSA), fields.delete('alg')
|
15
17
|
else
|
16
18
|
raise ArgumentError, "invalid 'alg' for JWS: #{fields['alg'].inspect}"
|
17
19
|
end
|
@@ -25,7 +27,8 @@ class JOSE::JWS::ALG_EDDSA < Struct.new(:sign_type)
|
|
25
27
|
# JOSE::JWS::ALG callbacks
|
26
28
|
|
27
29
|
def generate_key(fields)
|
28
|
-
|
30
|
+
okp_type = sign_type == :EdDSA ? :Ed25519 : sign_type
|
31
|
+
return JOSE::JWS::ALG.generate_key([:okp, okp_type], sign_type.to_s)
|
29
32
|
end
|
30
33
|
|
31
34
|
def sign(jwk, message)
|
data/lib/jose/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jose
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Bennett
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-07-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hamster
|