right_support 2.14.0 → 2.14.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|