Class: RightSupport::Crypto::SignedHash Deprecated

Inherits:
Object
  • Object
show all
Defined in:
lib/right_support/crypto/signed_hash.rb

Overview

Deprecated.

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.

See Also:

  • OpenSSL::PKey
  • Digest

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

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!

Parameters:

  • data (Hash) (defaults to: {})

    the actual data that is to be signed

  • key (OpenSSL::PKey::PKey) (defaults to: nil)
  • digest (Digest::Base, OpenSSL::Digest) (defaults to: nil)

    hash function to use

  • encoding (#dump) (defaults to: DefaultEncoding)
  • envelope (nil, :none, :right_support, :jwt) (defaults to: nil)

    serialization format and encryption scheme; default depends on key type

  • private_key (OpenSSL::PKey::PKey) (defaults to: nil)

    deprecated – pass key instead

  • public_key (OpenSSL::PKey::PKey) (defaults to: nil)

    deprecated – pass key instead



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.

Returns:

  • (Boolean)


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

Returns:

  • (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.

Parameters:

  • expires_at (Time)

Returns:

  • (String)

    a binary signature of the hash’s contents



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.

Returns:

  • (String)

    a signed JWT with the specified expiration timestamp



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.

Parameters:

  • signature (String)

    a binary signature to verify

  • expires_at (Time)

Returns:

  • (true)

    if the signature and expiration verify OK

  • (false)

    if the signature or expiration failed to verify



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.

Parameters:

  • signature (String)

    a binary signature to verify

  • expires_at (Time)

Returns:

  • (true)

    always returns true (except when it raises)

Raises:



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.message}"
end