right_support 2.14.0 → 2.14.1
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/lib/right_support/crypto/signed_hash.rb +259 -101
- data/lib/right_support/crypto.rb +4 -0
- data/lib/right_support/data/base64_url.rb +27 -0
- data/lib/right_support/data.rb +1 -0
- data/lib/right_support/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1dca63bf8c124604e44a21a45d56515e63fc6760
|
4
|
+
data.tar.gz: 224209a39c20b16906f9688755ea12b16957655e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab60e79b75e481d2c374fecf4f59f5c69bade7f859b3ad34d025b397cfb431bce2c6ef769f50406723b9170c2391c089e94255a7386760884a52b41ede8c9684
|
7
|
+
data.tar.gz: 7fc92ba8a0e765a51c043bc39ab11fcf664d2c2fad5330b18dcb42c4257a55b91f1726889b941c06a7d83c1f63c220e853903a6b74c7aefde10fecca00406a6c
|
@@ -24,37 +24,18 @@ require 'digest/md5'
|
|
24
24
|
require 'digest/sha1'
|
25
25
|
require 'digest/sha2'
|
26
26
|
require 'openssl'
|
27
|
+
require 'base64'
|
27
28
|
|
28
29
|
module RightSupport::Crypto
|
29
30
|
# An easy way to compute digital signatures of data contained in a Ruby hash. To work with
|
30
31
|
# signed hashes, you must first obtain an asymmetric key pair (any subclass of OpenSSL::PKey);
|
31
32
|
# you can generate it from scratch or load it from a file on disk.
|
32
33
|
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
# - The hash algorithm used to compute a message digest of the byte stream
|
37
|
-
# - The OpenSSL API level used (EVP or raw crypto API)
|
34
|
+
# This class supports RSA, DSA and ECDSA signature methods. When used with
|
35
|
+
# ECDSA, it _only_ supports JWT envelope format. The intent is to enable
|
36
|
+
# consumers of this class to switch to JSON Web Token in the future.
|
38
37
|
#
|
39
|
-
#
|
40
|
-
# and key length. This occasionally constrains your choice of hash algorithm; for instance,
|
41
|
-
# a 512-bit RSA key would not be sufficiently long to create signatures of a SHA3-512 hash
|
42
|
-
# due to the mathematical underpinnings of the RSA cipher. In practice this is not an issue,
|
43
|
-
# because you should be using strong RSA keys (2048 bit or higher) for security reasons,
|
44
|
-
# and even the strongest hash algorithms do not exceed 512-bit output.
|
45
|
-
#
|
46
|
-
# SignedHash provides reasonable defaults for the other three factors:
|
47
|
-
# - JSON for message encoding (Yajl gem, JSON gem, Oj gem or built-in Ruby 1.9 JSON)
|
48
|
-
# - SHA1 for message digest
|
49
|
-
# - raw crypto API (for compatibility with older RightSupport versions)
|
50
|
-
#
|
51
|
-
# If you are adopting SignedHash for a new use case, it's best to use the default
|
52
|
-
# encoding and message digest, but specify :envelope=>true to use the OpenSSL EVP
|
53
|
-
# API! Using an envelope provides better protection against various cryptographic
|
54
|
-
# attacks and ensures that the sign and verify operations can't be used.
|
55
|
-
#
|
56
|
-
# SignedHash defaults to raw-crypto signatures for compatibility reasons, but
|
57
|
-
# with RightSupport v3 the raw-crypto will be deprecated and EVP will be used by default.
|
38
|
+
# @deprecated please use JSON Web Token instead of this class!
|
58
39
|
#
|
59
40
|
# @see OpenSSL::PKey
|
60
41
|
# @see Digest
|
@@ -77,40 +58,77 @@ module RightSupport::Crypto
|
|
77
58
|
DefaultEncoding = nil
|
78
59
|
end unless defined?(DefaultEncoding)
|
79
60
|
|
80
|
-
|
81
|
-
|
82
|
-
:envelope => false,
|
83
|
-
:encoding => DefaultEncoding,
|
84
|
-
}
|
85
|
-
|
86
|
-
# Mapping of Ruby built-in hash algorithms to their OpenSSL counterparts
|
61
|
+
# List of acceptable Hash algorihtms + map of Ruby builtins to OpenSSL
|
62
|
+
# counterparts.
|
87
63
|
DIGEST_MAP = {
|
88
64
|
Digest::MD5 => OpenSSL::Digest::MD5,
|
89
65
|
Digest::SHA1 => OpenSSL::Digest::SHA1,
|
90
66
|
Digest::SHA2 => OpenSSL::Digest::SHA256,
|
91
|
-
|
67
|
+
OpenSSL::Digest::MD5 => OpenSSL::Digest::MD5,
|
68
|
+
OpenSSL::Digest::SHA1 => OpenSSL::Digest::SHA1,
|
69
|
+
OpenSSL::Digest::SHA256 => OpenSSL::Digest::SHA256,
|
70
|
+
OpenSSL::Digest::SHA384 => OpenSSL::Digest::SHA384,
|
71
|
+
OpenSSL::Digest::SHA512 => OpenSSL::Digest::SHA512,
|
72
|
+
}.freeze
|
73
|
+
|
74
|
+
# Digest output sizes (in bits)
|
75
|
+
HASHSIZE_MAP = {
|
76
|
+
Digest::MD5 => 128,
|
77
|
+
Digest::SHA1 => 160,
|
78
|
+
Digest::SHA2 => 256,
|
79
|
+
OpenSSL::Digest::MD5 => 128,
|
80
|
+
OpenSSL::Digest::SHA1 => 160,
|
81
|
+
OpenSSL::Digest::SHA256 => 256,
|
82
|
+
OpenSSL::Digest::SHA384 => 384,
|
83
|
+
OpenSSL::Digest::SHA512 => 512,
|
84
|
+
}.freeze
|
92
85
|
|
93
86
|
# Create a new sign/verify context, passing in a Hash full of data that is to be signed or
|
94
87
|
# verified. The new SignedHash will store a reference to the raw data, so be careful not to
|
95
88
|
# modify the data hash in a way that will influence the outcome of sign/verify!
|
96
89
|
#
|
97
|
-
# @param [Hash]
|
98
|
-
# @
|
99
|
-
# @
|
100
|
-
# @
|
101
|
-
# @
|
102
|
-
# @
|
90
|
+
# @param [Hash] data the actual data that is to be signed
|
91
|
+
# @param [OpenSSL::PKey::PKey] key
|
92
|
+
# @param [Digest::Base, OpenSSL::Digest] digest hash function to use
|
93
|
+
# @param [#dump] encoding
|
94
|
+
# @param [nil,:none,:right_support,:jwt] envelope serialization format and encryption scheme; default depends on key type
|
95
|
+
# @param [OpenSSL::PKey::PKey] private_key deprecated -- pass key instead
|
96
|
+
# @param [OpenSSL::PKey::PKey] public_key deprecated -- pass key instead
|
97
|
+
def initialize(data={}, key=nil, digest:nil, encoding:DefaultEncoding, envelope:nil, private_key:nil, public_key:nil)
|
98
|
+
# Cope with legacy parameter
|
99
|
+
if key.nil?
|
100
|
+
key = private_key || public_key
|
101
|
+
warn(':private_key and :public_key are deprecated; please pass key as 2nd parameter') if key
|
102
|
+
end
|
103
|
+
|
104
|
+
@data = data
|
105
|
+
@encoding = encoding
|
106
|
+
@key = key
|
107
|
+
|
108
|
+
# Figure out envelope type
|
109
|
+
env = case envelope
|
110
|
+
when nil then guess_envelope
|
111
|
+
when :none, false then :none
|
112
|
+
when :right_support, true then :right_support
|
113
|
+
when :jwt then :jwt
|
114
|
+
else raise ArgumentError.new("Unsupported envelope #{envelope.inspect}")
|
115
|
+
end
|
116
|
+
@envelope = env
|
117
|
+
|
118
|
+
# Figure out digest algorithm
|
119
|
+
@digest = digest || guess_digest
|
120
|
+
|
121
|
+
check_parameters
|
122
|
+
end
|
123
|
+
|
124
|
+
# Compute a signature and return a JSON Web Token representation of
|
125
|
+
# this hash.
|
103
126
|
#
|
104
|
-
# @
|
105
|
-
def
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
@encoding = opts[:encoding]
|
110
|
-
@envelope = !!opts[:envelope]
|
111
|
-
@public_key = opts[:public_key]
|
112
|
-
@private_key = opts[:private_key]
|
113
|
-
duck_type_check
|
127
|
+
# @return [String] a signed JWT with the specified expiration timestamp
|
128
|
+
def to_jwt(expires_at)
|
129
|
+
prefix, sig = sign_with_canonical_representation(expires_at)
|
130
|
+
sig = RightSupport::Data::Base64URL.encode(sig)
|
131
|
+
"#{prefix}.#{sig}"
|
114
132
|
end
|
115
133
|
|
116
134
|
# Produce a digital signature of the hash contents, including the expiration timestamp
|
@@ -120,25 +138,8 @@ module RightSupport::Crypto
|
|
120
138
|
# @param [Time] expires_at
|
121
139
|
# @return [String] a binary signature of the hash's contents
|
122
140
|
def sign(expires_at)
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
metadata = {:expires_at => expires_at}
|
127
|
-
encoded = encode(canonicalize(frame(@hash, metadata)))
|
128
|
-
|
129
|
-
if @envelope
|
130
|
-
digest = DIGEST_MAP[@digest].new(encoded)
|
131
|
-
if @private_key.respond_to?(:dsa_sign_asn1)
|
132
|
-
# DSA signature with ASN.1 encoding
|
133
|
-
@private_key.dsa_sign_asn1(digest.digest)
|
134
|
-
else
|
135
|
-
# RSA/DSA signature as specified in PKCS #1 v1.5
|
136
|
-
@private_key.sign(digest, encoded)
|
137
|
-
end
|
138
|
-
else
|
139
|
-
digest = @digest.new.update(encoded).digest
|
140
|
-
@private_key.private_encrypt(digest)
|
141
|
-
end
|
141
|
+
_, sig = sign_with_canonical_representation(expires_at)
|
142
|
+
sig
|
142
143
|
end
|
143
144
|
|
144
145
|
# Verify a digital signature of the hash's contents. In order for the signature to verify,
|
@@ -151,30 +152,48 @@ module RightSupport::Crypto
|
|
151
152
|
# @raise [ExpiredSignature] if the signature is expired
|
152
153
|
# @raise [InvalidSignature] if the signature is invalid
|
153
154
|
def verify!(signature, expires_at)
|
154
|
-
raise ArgumentError, "Cannot verify; missing public_key" unless @
|
155
|
+
raise ArgumentError, "Cannot verify; missing public_key" unless @key
|
155
156
|
|
156
157
|
metadata = {:expires_at => expires_at}
|
157
|
-
plaintext = encode(
|
158
|
+
plaintext = encode(canonicalize(frame(@data, metadata)))
|
158
159
|
|
159
|
-
|
160
|
+
case @envelope
|
161
|
+
when :none
|
162
|
+
digest = @digest.new.update(plaintext).digest
|
163
|
+
# raw RSA decryption
|
164
|
+
actual = @key.public_decrypt(signature)
|
165
|
+
raise InvalidSignature, "Signature mismatch: expected #{digest}, got #{actual}" unless actual == digest
|
166
|
+
when :jwt
|
167
|
+
if @key.respond_to?(:dsa_verify_asn1)
|
168
|
+
# DSA signature with JWT-compatible encoding
|
169
|
+
digest = @digest.new.update(plaintext).digest
|
170
|
+
signature = raw_to_asn1(signature, @key)
|
171
|
+
result = @key.dsa_verify_asn1(digest, signature)
|
172
|
+
raise InvalidSignature, "Signature mismatch: DSA verify failed" unless result
|
173
|
+
elsif @key.respond_to?(:verify)
|
174
|
+
digest = @digest.new
|
175
|
+
result = @key.verify(digest, signature, plaintext)
|
176
|
+
raise InvalidSignature, "Signature mismatch: verify failed" unless result
|
177
|
+
else
|
178
|
+
raise NotImplementedError, "Cannot verify JWT with #{@key.class.name}"
|
179
|
+
end
|
180
|
+
when :right_support
|
160
181
|
digest = DIGEST_MAP[@digest].new
|
161
|
-
if @
|
182
|
+
if @key.respond_to?(:dsa_verify_asn1)
|
162
183
|
# DSA signature with ASN.1 encoding
|
163
|
-
@
|
184
|
+
@key.dsa_verify_asn1(digest.digest, signature)
|
164
185
|
else
|
165
186
|
# RSA/DSA signature as specified in PKCS #1 v1.5
|
166
|
-
result = @
|
187
|
+
result = @key.verify(digest, signature, plaintext)
|
167
188
|
end
|
168
189
|
raise InvalidSignature, "Signature verification failed" unless true == result
|
169
|
-
else
|
170
|
-
expected = @digest.new.update(plaintext).digest
|
171
|
-
actual = @public_key.public_decrypt(signature)
|
172
|
-
raise InvalidSignature, "Signature mismatch: expected #{expected}, got #{actual}" unless actual == expected
|
173
190
|
end
|
174
191
|
|
175
192
|
raise ExpiredSignature, "The signature has expired (or expires_at is not a Time)" unless time_check(expires_at)
|
176
193
|
|
177
194
|
true
|
195
|
+
rescue OpenSSL::PKey::RSAError => e
|
196
|
+
raise InvalidSignature, "Signature mismatch: #{e.message}"
|
178
197
|
end
|
179
198
|
|
180
199
|
# Verify a digital signature of the hash's contents. In order for the signature to verify,
|
@@ -187,53 +206,131 @@ module RightSupport::Crypto
|
|
187
206
|
# @return [false] if the signature or expiration failed to verify
|
188
207
|
def verify(signature, expires_at)
|
189
208
|
verify!(signature, expires_at)
|
190
|
-
rescue
|
209
|
+
rescue ExpiredSignature, InvalidSignature => e
|
191
210
|
false
|
192
211
|
end
|
193
212
|
|
194
213
|
# Free the inner Hash.
|
195
214
|
def method_missing(meth, *args)
|
196
|
-
@
|
215
|
+
@data.__send__(meth, *args)
|
197
216
|
end
|
198
217
|
|
199
218
|
# Free the inner Hash.
|
200
219
|
def respond_to?(meth, include_all=false)
|
201
|
-
super || @
|
220
|
+
super || @data.respond_to?(meth)
|
202
221
|
end
|
203
222
|
|
204
223
|
def respond_to_missing?(meth, include_all=false)
|
205
|
-
super || @
|
224
|
+
super || @data.respond_to?(meth, include_all)
|
206
225
|
end
|
207
226
|
|
208
227
|
private
|
209
228
|
|
210
|
-
|
229
|
+
# @return [Array] a pair of strings: the exact message that was signed, and its raw-binary signature
|
230
|
+
def sign_with_canonical_representation(expires_at)
|
231
|
+
raise ArgumentError, "Cannot sign; missing private_key" unless @key
|
232
|
+
raise ArgumentError, "expires_at must be a Time in the future" unless time_check(expires_at)
|
233
|
+
|
234
|
+
metadata = {:expires_at => expires_at}
|
235
|
+
plaintext = encode(canonicalize(frame(@data, metadata)))
|
236
|
+
|
237
|
+
case @envelope
|
238
|
+
when :none
|
239
|
+
digest = @digest.new.update(plaintext).digest
|
240
|
+
# raw RSA encryption; output is a single bignum
|
241
|
+
sig = @key.private_encrypt(digest)
|
242
|
+
when :jwt
|
243
|
+
if @key.respond_to?(:dsa_sign_asn1)
|
244
|
+
# raw ECDSA signature; output are two bignums (r, s) stuck together
|
245
|
+
# with no delimiter
|
246
|
+
digest = @digest.new.update(plaintext).digest
|
247
|
+
asn1 = @key.dsa_sign_asn1(digest)
|
248
|
+
sig = asn1_to_raw(asn1, @key)
|
249
|
+
elsif @key.respond_to?(:sign)
|
250
|
+
# RSA/DSA signature as specified in PKCS #1 v1.5
|
251
|
+
digest = @digest.new
|
252
|
+
sig = @key.sign(digest, plaintext)
|
253
|
+
else
|
254
|
+
raise NotImplementedError, "Cannot sign JWT with a #{@key.class.name}"
|
255
|
+
end
|
256
|
+
when :right_support
|
257
|
+
digest = DIGEST_MAP[@digest].new(plaintext)
|
258
|
+
if @key.respond_to?(:dsa_sign_asn1)
|
259
|
+
# DSA signature with ASN.1 encoding
|
260
|
+
sig = @key.dsa_sign_asn1(digest.digest)
|
261
|
+
else
|
262
|
+
# RSA/DSA signature as specified in PKCS #1 v1.5
|
263
|
+
sig = @key.sign(digest, plaintext)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
return plaintext, sig
|
268
|
+
end
|
269
|
+
|
270
|
+
# @raise [ArgumentError] if there is anything wrong with the crypto parameters
|
271
|
+
def check_parameters
|
272
|
+
# raises if key/digest are not compatible with JOSE
|
273
|
+
begin
|
274
|
+
jose_header if @envelope == :jwt
|
275
|
+
rescue NotImplementedError => e
|
276
|
+
raise ArgumentError.new(e.message)
|
277
|
+
end
|
278
|
+
|
211
279
|
unless DIGEST_MAP.key?(@digest)
|
212
|
-
raise ArgumentError, "Digest must be
|
280
|
+
raise ArgumentError, "Digest must be one of Digest::* class or OpenSSL::Digest::*"
|
213
281
|
end
|
214
|
-
unless @encoding.respond_to?(
|
282
|
+
unless @encoding.respond_to?(:dump)
|
215
283
|
raise ArgumentError, "Encoding class/module/object must respond to .dump method"
|
216
284
|
end
|
217
285
|
|
218
286
|
if @envelope
|
219
|
-
if @
|
287
|
+
if @key && !@key.respond_to?(:verify)
|
220
288
|
raise ArgumentError, "Public key must respond to :verify"
|
221
289
|
end
|
222
|
-
if @
|
290
|
+
if @key && !@key.respond_to?(:sign)
|
223
291
|
raise ArgumentError, "Private key must respond to :sign"
|
224
292
|
end
|
225
293
|
else
|
226
|
-
if @
|
294
|
+
if @key && !@key.respond_to?(:public_decrypt)
|
227
295
|
raise ArgumentError, "Public key must respond to :public_decrypt"
|
228
296
|
end
|
229
|
-
if @
|
297
|
+
if @key && !@key.respond_to?(:private_encrypt)
|
230
298
|
raise ArgumentError, "Private key must respond to :private_encrypt"
|
231
299
|
end
|
232
300
|
end
|
233
301
|
end
|
234
302
|
|
235
|
-
|
236
|
-
|
303
|
+
# Figure out a default envelope if none is provided
|
304
|
+
def guess_envelope
|
305
|
+
case @key
|
306
|
+
when OpenSSL::PKey::EC
|
307
|
+
:jwt
|
308
|
+
else
|
309
|
+
:none
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# Figure out a suitable default digest if none is provided
|
314
|
+
def guess_digest
|
315
|
+
case @key
|
316
|
+
when OpenSSL::PKey::DSA
|
317
|
+
OpenSSL::Digest::SHA1
|
318
|
+
when OpenSSL::PKey::EC
|
319
|
+
case @key.group.degree
|
320
|
+
when 256 then OpenSSL::Digest::SHA256
|
321
|
+
when 384 then OpenSSL::Digest::SHA384
|
322
|
+
when 521 then OpenSSL::Digest::SHA512
|
323
|
+
else
|
324
|
+
raise ArgumentError, "Cannot guess digest; please pass it as an option"
|
325
|
+
end
|
326
|
+
when OpenSSL::PKey::RSA
|
327
|
+
case @envelope
|
328
|
+
when :jwt then OpenSSL::Digest::SHA256
|
329
|
+
else OpenSSL::Digest::SHA1
|
330
|
+
end
|
331
|
+
else
|
332
|
+
OpenSSL::Digest::SHA1
|
333
|
+
end
|
237
334
|
end
|
238
335
|
|
239
336
|
# Ensure that an expiration time is in the future.
|
@@ -241,21 +338,28 @@ module RightSupport::Crypto
|
|
241
338
|
t.is_a?(Time) && (t >= Time.now)
|
242
339
|
end
|
243
340
|
|
244
|
-
# Incorporate the hash and its signature metadata into an enclosing hash.
|
245
|
-
def frame(data, metadata) # :nodoc:
|
246
|
-
{:data => data, :metadata => metadata}
|
247
|
-
end
|
248
|
-
|
249
341
|
# Encode a canonicalized representation of the hash.
|
250
|
-
def encode(input)
|
251
|
-
@
|
342
|
+
def encode(input) # :nodoc:
|
343
|
+
case @envelope
|
344
|
+
when :none, :right_support
|
345
|
+
@encoding.dump(input)
|
346
|
+
when :jwt
|
347
|
+
input.map { |m| RightSupport::Data::Base64URL.encode(m) }.join('.')
|
348
|
+
end
|
252
349
|
end
|
253
350
|
|
254
|
-
#
|
255
|
-
#
|
256
|
-
#
|
257
|
-
#
|
351
|
+
# If envelope is :jwt, return the input with each element mapped
|
352
|
+
# to its encoded form, but otherwise unchanged in any way.
|
353
|
+
#
|
354
|
+
# For any other envelope type, canonicalize the hash (and any nested data)
|
355
|
+
# by transforming it deterministically into a structure of arrays-in-arrays
|
356
|
+
# whose elements are ordered according to the lexical ordering of hash keys.
|
357
|
+
# Canonicalization ensures that the signer and verifier agree on the
|
358
|
+
# contents of the thing being signed irrespective of Ruby version, CPU
|
359
|
+
# architecture, etc.
|
258
360
|
def canonicalize(input) # :nodoc:
|
361
|
+
return input.map { |i| @encoding.dump(i) } if @envelope == :jwt
|
362
|
+
|
259
363
|
case input
|
260
364
|
when Hash
|
261
365
|
# Hash is the only complex case. We canonicalize a Hash as an Array of pairs, each of which
|
@@ -296,5 +400,59 @@ module RightSupport::Crypto
|
|
296
400
|
|
297
401
|
output
|
298
402
|
end
|
403
|
+
|
404
|
+
# Incorporate the hash and its signature metadata into a Hash or Array
|
405
|
+
# that represents the logical layout of the thing to be signed or verified.
|
406
|
+
def frame(data, metadata) # :nodoc:
|
407
|
+
case @envelope
|
408
|
+
when :none, :right_support
|
409
|
+
# Proprietary framing
|
410
|
+
{:data => data, :metadata => metadata}
|
411
|
+
when :jwt
|
412
|
+
# JOSE framing; see http://jose.readthedocs.io
|
413
|
+
payload = data.dup
|
414
|
+
payload['exp'] = metadata[:expires_at].to_i
|
415
|
+
[jose_header, payload]
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# @return [Hash] JOSE header defining document type (JWT) and encryption algorithm
|
420
|
+
def jose_header
|
421
|
+
key = @key || @key
|
422
|
+
|
423
|
+
prefix = case key
|
424
|
+
when OpenSSL::PKey::EC
|
425
|
+
'ES'
|
426
|
+
when OpenSSL::PKey::RSA
|
427
|
+
'RS'
|
428
|
+
else
|
429
|
+
raise NotImplementedError, "Cannot use a #{key.class.name} with JWT envelope"
|
430
|
+
end
|
431
|
+
|
432
|
+
size = HASHSIZE_MAP[@digest]
|
433
|
+
unless size
|
434
|
+
raise NotImplementedError, "Cannot use #{@digest.name} with JWT envelope and this kind of key"
|
435
|
+
end
|
436
|
+
|
437
|
+
{'typ' => 'JWT', 'alg' => "#{prefix}#{size}"}
|
438
|
+
end
|
439
|
+
|
440
|
+
# Convert ASN1-encoded pair of integers into raw pair of concatenated bignums
|
441
|
+
# This only works for OpenSSL::PKey::EC.
|
442
|
+
# See https://github.com/jwt/ruby-jwt/blob/master/lib/jwt.rb#L166
|
443
|
+
def asn1_to_raw(signature, private_key) # :nodoc:
|
444
|
+
byte_size = (private_key.group.degree + 7) / 8
|
445
|
+
OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
|
446
|
+
end
|
447
|
+
|
448
|
+
# Convert raw pair of concatenated bignums into ASN1-encoded pair of integers.
|
449
|
+
# This only works for OpenSSL::PKey::EC.
|
450
|
+
# https://github.com/jwt/ruby-jwt/blob/master/lib/jwt.rb#L159
|
451
|
+
def raw_to_asn1(signature, public_key) # :nodoc:
|
452
|
+
byte_size = (public_key.group.degree + 7) / 8
|
453
|
+
r = signature[0..(byte_size - 1)]
|
454
|
+
s = signature[byte_size..-1] || ''
|
455
|
+
OpenSSL::ASN1::Sequence.new([r, s].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
|
456
|
+
end
|
299
457
|
end
|
300
458
|
end
|
data/lib/right_support/crypto.rb
CHANGED
@@ -25,6 +25,10 @@ module RightSupport
|
|
25
25
|
# A namespace for cryptographic functionality.
|
26
26
|
#
|
27
27
|
module Crypto
|
28
|
+
# Exception indicating that a cryptographic message payload contains
|
29
|
+
# something whose state can't cleanly signed, encrypted or verified.
|
30
|
+
class InvalidPayload < StandardError; end
|
31
|
+
|
28
32
|
# Exception indicating that a cryptographic signature is invalid. This can happen for several
|
29
33
|
# reasons:
|
30
34
|
# * someone tampered with the signature
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module RightSupport::Data
|
4
|
+
# Implements the base64url encoding mechanism as described in RFC7515: JSON
|
5
|
+
# Web Signature (JWS).
|
6
|
+
#
|
7
|
+
# This encoding is similar to base64, but omits newlines and padding
|
8
|
+
# `=` characters, and uses the `-` and `_` characters in place of the `+` and
|
9
|
+
# `/` characters. This makes it suitable for use inside URLs, cookies, and
|
10
|
+
# other "webby" contexts where we want to avoid text expansion due to URL
|
11
|
+
# encoding.
|
12
|
+
module Base64URL
|
13
|
+
# @param [String] str ASCII string to encode
|
14
|
+
# @return [String] base64 representation of str
|
15
|
+
def self.encode(text)
|
16
|
+
Base64.encode64(text).tr('+/', '-_').gsub(/[\n=]/, '')
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [String] str base64 to decode to ASCII
|
20
|
+
# @return [String] plain ASCII
|
21
|
+
def self.decode(b64)
|
22
|
+
b64 = b64.dup
|
23
|
+
b64 += '=' * (4 - b64.length.modulo(4))
|
24
|
+
Base64.decode64(b64.tr('-_', '+/'))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/right_support/data.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: right_support
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.14.
|
4
|
+
version: 2.14.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tony Spataro
|
@@ -13,7 +13,7 @@ authors:
|
|
13
13
|
autorequire:
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
|
-
date: 2016-12-
|
16
|
+
date: 2016-12-20 00:00:00.000000000 Z
|
17
17
|
dependencies: []
|
18
18
|
description: A toolkit of useful, reusable foundation code created by RightScale.
|
19
19
|
email: support@rightscale.com
|
@@ -33,6 +33,7 @@ files:
|
|
33
33
|
- lib/right_support/crypto.rb
|
34
34
|
- lib/right_support/crypto/signed_hash.rb
|
35
35
|
- lib/right_support/data.rb
|
36
|
+
- lib/right_support/data/base64_url.rb
|
36
37
|
- lib/right_support/data/hash_tools.rb
|
37
38
|
- lib/right_support/data/mash.rb
|
38
39
|
- lib/right_support/data/serializer.rb
|