Class: RightSupport::Crypto::SignedHash
- Defined in:
- lib/right_support/crypto/signed_hash.rb
Overview
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.
Signature computation is influenced by four factors:
- The digital signature algorithm and key length
- The encoding used to serialize the hash contents to a byte stream
- The hash algorithm used to compute a digest of the byte stream
- The OpenSSL API level used (EVP or raw crypto API)
You are responsible for providing the PKey object, which determines the signature algorithm and key length. This occasionally constrains your choice of hash algorithm; for instance, a 512-bit RSA key would not be sufficiently long to create signatures of a SHA3-512 hash due to the mathematical underpinnings of the RSA cipher. In practice this is not an issue, because you should be using strong RSA keys (2048 bit or higher) for security reasons, and even the strongest hash algorithms do not exceed 512-bit output.
SignedHash provides reasonable defaults for the other three factors:
- JSON for encoding (Yajl gem, JSON gem, Oj gem or built-in Ruby 1.9 JSON)
- SHA1 for digest
- raw crypto API (for compatibility with older RightSupport versions)
If you are adopting SignedHash for a new use case, it’s best to use the default encoding and message digest, but specify :envelope=>true to use the OpenSSL EVP API! Using an envelope provides better protection against various cryptographic attacks and ensures that the sign and verify operations can’t be used.
SignedHash defaults to raw-crypto signatures for compatibility reasons, but with RightSupport v3 the raw-crypto will be deprecated and EVP will be used by default.
Constant Summary collapse
- DefaultEncoding =
nil- DEFAULT_OPTIONS =
{ :digest => Digest::SHA1, :envelope => false, :encoding => DefaultEncoding, }
- DIGEST_MAP =
Mapping of Ruby built-in hash algorithms to their OpenSSL counterparts
{ Digest::MD5 => OpenSSL::Digest::MD5, Digest::SHA1 => OpenSSL::Digest::SHA1, Digest::SHA2 => OpenSSL::Digest::SHA256, }
Instance Method Summary collapse
-
#initialize(hash = {}, opts = {}) ⇒ 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) ⇒ Boolean
Free the inner Hash.
-
#sign(expires_at) ⇒ String
Produce a digital signature of the hash contents, including the expiration timestamp of the signature.
-
#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(hash = {}, opts = {}) ⇒ 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!
105 106 107 108 109 110 111 112 113 114 |
# File 'lib/right_support/crypto/signed_hash.rb', line 105 def initialize(hash={}, opts={}) opts = DEFAULT_OPTIONS.merge(opts) @hash = hash @digest = opts[:digest] @encoding = opts[:encoding] @envelope = !!opts[:envelope] @public_key = opts[:public_key] @private_key = opts[:private_key] duck_type_check end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(meth, *args) ⇒ Object
Free the inner Hash.
184 185 186 |
# File 'lib/right_support/crypto/signed_hash.rb', line 184 def method_missing(meth, *args) @hash.__send__(meth, *args) end |
Instance Method Details
#respond_to?(meth) ⇒ Boolean
Free the inner Hash.
189 190 191 |
# File 'lib/right_support/crypto/signed_hash.rb', line 189 def respond_to?(meth) super || @hash.respond_to?(meth) 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.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/right_support/crypto/signed_hash.rb', line 122 def sign(expires_at) raise ArgumentError, "Cannot sign; missing private_key" unless @private_key raise ArgumentError, "expires_at must be a Time in the future" unless time_check(expires_at) = {:expires_at => expires_at} encoded = encode(canonicalize(frame(@hash, ))) if @envelope digest = DIGEST_MAP[@digest].new(encoded) @private_key.sign(digest, encoded) else digest = @digest.new.update(encoded).digest @private_key.private_encrypt(digest) end 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.
177 178 179 180 181 |
# File 'lib/right_support/crypto/signed_hash.rb', line 177 def verify(signature, expires_at) verify!(signature, expires_at) rescue Exception => 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.
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/right_support/crypto/signed_hash.rb', line 148 def verify!(signature, expires_at) raise ArgumentError, "Cannot verify; missing public_key" unless @public_key = {:expires_at => expires_at} plaintext = encode( canonicalize( frame(@hash, ) ) ) if @envelope digest = DIGEST_MAP[@digest].new result = @public_key.verify(digest, signature, plaintext) raise InvalidSignature, "Signature verification failed" unless true == result else expected = @digest.new.update(plaintext).digest actual = @public_key.public_decrypt(signature) raise InvalidSignature, "Signature mismatch: expected #{expected}, got #{actual}" unless actual == expected end raise ExpiredSignature, "The signature has expired (or expires_at is not a Time)" unless time_check(expires_at) true end |