Class: RightSupport::Crypto::SignedHash Deprecated
- Defined in:
- lib/right_support/crypto/signed_hash.rb
Overview
please use JSON Web Token instead of this class!
An easy way to compute digital signatures of data contained in a Ruby hash. To work with signed hashes, you must first obtain an asymmetric key pair (any subclass of OpenSSL::PKey); you can generate it from scratch or load it from a file on disk.
This class supports RSA, DSA and ECDSA signature methods. When used with ECDSA, it only supports JWT envelope format. The intent is to enable consumers of this class to switch to JSON Web Token in the future.
Constant Summary collapse
- DefaultEncoding =
nil
- DIGEST_MAP =
List of acceptable Hash algorihtms + map of Ruby builtins to OpenSSL counterparts.
{ Digest::MD5 => OpenSSL::Digest::MD5, Digest::SHA1 => OpenSSL::Digest::SHA1, Digest::SHA2 => OpenSSL::Digest::SHA256, OpenSSL::Digest::MD5 => OpenSSL::Digest::MD5, OpenSSL::Digest::SHA1 => OpenSSL::Digest::SHA1, OpenSSL::Digest::SHA256 => OpenSSL::Digest::SHA256, OpenSSL::Digest::SHA384 => OpenSSL::Digest::SHA384, OpenSSL::Digest::SHA512 => OpenSSL::Digest::SHA512, }.freeze
- HASHSIZE_MAP =
Digest output sizes (in bits)
{ Digest::MD5 => 128, Digest::SHA1 => 160, Digest::SHA2 => 256, OpenSSL::Digest::MD5 => 128, OpenSSL::Digest::SHA1 => 160, OpenSSL::Digest::SHA256 => 256, OpenSSL::Digest::SHA384 => 384, OpenSSL::Digest::SHA512 => 512, }.freeze
Instance Method Summary collapse
-
#initialize(data = {}, key = nil, digest: nil, encoding: DefaultEncoding, envelope: nil, private_key: nil, public_key: nil) ⇒ SignedHash
constructor
Create a new sign/verify context, passing in a Hash full of data that is to be signed or verified.
-
#method_missing(meth, *args) ⇒ Object
Free the inner Hash.
-
#respond_to?(meth, include_all = false) ⇒ Boolean
Free the inner Hash.
- #respond_to_missing?(meth, include_all = false) ⇒ Boolean
-
#sign(expires_at) ⇒ String
Produce a digital signature of the hash contents, including the expiration timestamp of the signature.
-
#to_jwt(expires_at) ⇒ String
Compute a signature and return a JSON Web Token representation of this hash.
-
#verify(signature, expires_at) ⇒ true, false
Verify a digital signature of the hash’s contents.
-
#verify!(signature, expires_at) ⇒ true
Verify a digital signature of the hash’s contents.
Constructor Details
#initialize(data = {}, key = nil, digest: nil, encoding: DefaultEncoding, envelope: nil, private_key: nil, public_key: nil) ⇒ SignedHash
Create a new sign/verify context, passing in a Hash full of data that is to be signed or verified. The new SignedHash will store a reference to the raw data, so be careful not to modify the data hash in a way that will influence the outcome of sign/verify!
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/right_support/crypto/signed_hash.rb', line 97 def initialize(data={}, key=nil, digest:nil, encoding:DefaultEncoding, envelope:nil, private_key:nil, public_key:nil) # Cope with legacy parameter if key.nil? key = private_key || public_key warn(':private_key and :public_key are deprecated; please pass key as 2nd parameter') if key end @data = data @encoding = encoding @key = key # Figure out envelope type env = case envelope when nil then guess_envelope when :none, false then :none when :right_support, true then :right_support when :jwt then :jwt else raise ArgumentError.new("Unsupported envelope #{envelope.inspect}") end @envelope = env # Figure out digest algorithm @digest = digest || guess_digest check_parameters end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(meth, *args) ⇒ Object
Free the inner Hash.
214 215 216 |
# File 'lib/right_support/crypto/signed_hash.rb', line 214 def method_missing(meth, *args) @data.__send__(meth, *args) end |
Instance Method Details
#respond_to?(meth, include_all = false) ⇒ Boolean
Free the inner Hash.
219 220 221 |
# File 'lib/right_support/crypto/signed_hash.rb', line 219 def respond_to?(meth, include_all=false) super || @data.respond_to?(meth) end |
#respond_to_missing?(meth, include_all = false) ⇒ Boolean
223 224 225 |
# File 'lib/right_support/crypto/signed_hash.rb', line 223 def respond_to_missing?(meth, include_all=false) super || @data.respond_to?(meth, include_all) end |
#sign(expires_at) ⇒ String
Produce a digital signature of the hash contents, including the expiration timestamp of the signature. The caller must provide the exact same hash and expires_at in order to successfully verify the signature.
140 141 142 143 |
# File 'lib/right_support/crypto/signed_hash.rb', line 140 def sign(expires_at) _, sig = sign_with_canonical_representation(expires_at) sig end |
#to_jwt(expires_at) ⇒ String
Compute a signature and return a JSON Web Token representation of this hash.
128 129 130 131 132 |
# File 'lib/right_support/crypto/signed_hash.rb', line 128 def to_jwt(expires_at) prefix, sig = sign_with_canonical_representation(expires_at) sig = RightSupport::Data::Base64URL.encode(sig) "#{prefix}.#{sig}" end |
#verify(signature, expires_at) ⇒ true, false
Verify a digital signature of the hash’s contents. In order for the signature to verify, the expires_at, signature and hash contents must be identical to those used by the signer.
207 208 209 210 211 |
# File 'lib/right_support/crypto/signed_hash.rb', line 207 def verify(signature, expires_at) verify!(signature, expires_at) rescue ExpiredSignature, InvalidSignature => e false end |
#verify!(signature, expires_at) ⇒ true
Verify a digital signature of the hash’s contents. In order for the signature to verify, the expires_at, signature and hash contents must be identical to those used by the signer.
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/right_support/crypto/signed_hash.rb', line 154 def verify!(signature, expires_at) raise ArgumentError, "Cannot verify; missing public_key" unless @key = {:expires_at => expires_at} plaintext = encode(canonicalize(frame(@data, ))) case @envelope when :none digest = @digest.new.update(plaintext).digest # raw RSA decryption actual = @key.public_decrypt(signature) raise InvalidSignature, "Signature mismatch: expected #{digest}, got #{actual}" unless actual == digest when :jwt if @key.respond_to?(:dsa_verify_asn1) # DSA signature with JWT-compatible encoding digest = @digest.new.update(plaintext).digest signature = raw_to_asn1(signature, @key) result = @key.dsa_verify_asn1(digest, signature) raise InvalidSignature, "Signature mismatch: DSA verify failed" unless result elsif @key.respond_to?(:verify) digest = @digest.new result = @key.verify(digest, signature, plaintext) raise InvalidSignature, "Signature mismatch: verify failed" unless result else raise NotImplementedError, "Cannot verify JWT with #{@key.class.name}" end when :right_support digest = DIGEST_MAP[@digest].new if @key.respond_to?(:dsa_verify_asn1) # DSA signature with ASN.1 encoding @key.dsa_verify_asn1(digest.digest, signature) else # RSA/DSA signature as specified in PKCS #1 v1.5 result = @key.verify(digest, signature, plaintext) end raise InvalidSignature, "Signature verification failed" unless true == result end raise ExpiredSignature, "The signature has expired (or expires_at is not a Time)" unless time_check(expires_at) true rescue OpenSSL::PKey::RSAError => e raise InvalidSignature, "Signature mismatch: #{e.}" end |