Class: RightSupport::Crypto::SignedHash

Inherits:
Object
  • Object
show all
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 message 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 message encoding (Yajl gem, JSON gem, Oj gem or built-in Ruby 1.9 JSON)
- SHA1 for message 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.

See Also:

  • OpenSSL::PKey
  • Digest

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

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!

Options Hash (opts):

  • :digest (Class)

    hash-algorithm class from Ruby’s Digest module MD5, SHA1 or SHA2; default SHA1

  • :envelope (true, false)

    use the OpenSSL EVP API if true, or raw-crypto API if false; default false

  • :encoding (#dump)

    serialization method for dumping hash data; default DefaultEncoding

  • :public_key (OpenSSL::PKey)

    key to use when verifying digital signatures

  • :private_key (OpenSSL::PKey)

    key to use when computing digital signatures

See Also:



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.

Raises:

  • (ArgumentError)


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.

Raises:



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