class Flipper::Cloud::MessageVerifier

Constants

DEFAULT_VERSION

Public Class Methods

header(signature, timestamp, version = DEFAULT_VERSION) click to toggle source
# File lib/flipper/cloud/message_verifier.rb, line 11
def self.header(signature, timestamp, version = DEFAULT_VERSION)
  raise ArgumentError, "timestamp should be an instance of Time" unless timestamp.is_a?(Time)
  raise ArgumentError, "signature should be a string" unless signature.is_a?(String)
  "t=#{timestamp.to_i},#{version}=#{signature}"
end
new(secret:, version: DEFAULT_VERSION) click to toggle source
# File lib/flipper/cloud/message_verifier.rb, line 17
def initialize(secret:, version: DEFAULT_VERSION)
  @secret = secret
  @version = version || DEFAULT_VERSION

  raise ArgumentError, "secret should be a string" unless @secret.is_a?(String)
  raise ArgumentError, "version should be a string" unless @version.is_a?(String)
end

Public Instance Methods

generate(payload, timestamp) click to toggle source
# File lib/flipper/cloud/message_verifier.rb, line 25
def generate(payload, timestamp)
  raise ArgumentError, "timestamp should be an instance of Time" unless timestamp.is_a?(Time)
  raise ArgumentError, "payload should be a string" unless payload.is_a?(String)

  OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), @secret, "#{timestamp.to_i}.#{payload}")
end
header(signature, timestamp) click to toggle source
# File lib/flipper/cloud/message_verifier.rb, line 32
def header(signature, timestamp)
  self.class.header(signature, timestamp, @version)
end
verify(payload, header, tolerance: nil) click to toggle source

Public: Verifies the signature header for a given payload.

Raises a InvalidSignature in the following cases:

  • the header does not match the expected format

  • no signatures found with the expected scheme

  • no signatures matching the expected signature

  • a tolerance is provided and the timestamp is not within the tolerance

Returns true otherwise.

# File lib/flipper/cloud/message_verifier.rb, line 46
def verify(payload, header, tolerance: nil)
  begin
    timestamp, signatures = get_timestamp_and_signatures(header)
  rescue StandardError
    raise InvalidSignature, "Unable to extract timestamp and signatures from header"
  end

  if signatures.empty?
    raise InvalidSignature, "No signatures found with expected version #{@version}"
  end

  expected_sig = generate(payload, timestamp)
  unless signatures.any? { |s| secure_compare(expected_sig, s) }
    raise InvalidSignature, "No signatures found matching the expected signature for payload"
  end

  if tolerance && timestamp < Time.now - tolerance
    raise InvalidSignature, "Timestamp outside the tolerance zone (#{Time.at(timestamp)})"
  end

  true
end

Private Instance Methods

fixed_length_secure_compare(a, b) click to toggle source

Private

# File lib/flipper/cloud/message_verifier.rb, line 81
def fixed_length_secure_compare(a, b)
  raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
  l = a.unpack "C#{a.bytesize}"
  res = 0
  b.each_byte { |byte| res |= byte ^ l.shift }
  res == 0
end
get_timestamp_and_signatures(header) click to toggle source

Extracts the timestamp and the signature(s) with the desired version from the header

# File lib/flipper/cloud/message_verifier.rb, line 73
def get_timestamp_and_signatures(header)
  list_items = header.split(/,\s*/).map { |i| i.split("=", 2) }
  timestamp = Integer(list_items.select { |i| i[0] == "t" }[0][1])
  signatures = list_items.select { |i| i[0] == @version }.map { |i| i[1] }
  [Time.at(timestamp), signatures]
end
secure_compare(a, b) click to toggle source

Private

# File lib/flipper/cloud/message_verifier.rb, line 90
def secure_compare(a, b)
  fixed_length_secure_compare(::Digest::SHA256.digest(a), ::Digest::SHA256.digest(b)) && a == b
end