diff --git a/Gemfile.lock b/Gemfile.lock index fa5bf3e..a8d90b4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - gala (0.4.0) + gala (0.5.0) openssl (= 3.1.0) GEM @@ -10,6 +10,7 @@ GEM minitest (5.18.0) openssl (3.1.0) rake (12.3.3) + timecop (0.9.8) PLATFORMS ruby @@ -19,6 +20,7 @@ DEPENDENCIES gala! minitest rake (~> 12.0) + timecop BUNDLED WITH 1.17.3 diff --git a/gala.gemspec b/gala.gemspec index 4145221..fdb3eb0 100644 --- a/gala.gemspec +++ b/gala.gemspec @@ -26,4 +26,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler', '~> 1.14' spec.add_development_dependency 'rake', '~> 12.0' spec.add_development_dependency 'minitest' + spec.add_development_dependency 'timecop' end diff --git a/lib/gala/payment_token.rb b/lib/gala/payment_token.rb index e4ba976..0cb84d0 100644 --- a/lib/gala/payment_token.rb +++ b/lib/gala/payment_token.rb @@ -8,6 +8,7 @@ class PaymentToken LEAF_CERTIFICATE_OID = "1.2.840.113635.100.6.29" INTERMEDIATE_CERTIFICATE_OID = "1.2.840.113635.100.6.2.14" APPLE_ROOT_CERT = File.read(File.dirname(__FILE__) + "/resources/AppleRootCA-G3.pem") + SIGNATURE_VALIDITY_TIME_WINDOW = 5 * 60 # 5 minutes attr_accessor :version, :data, :signature, :transaction_id, :ephemeral_public_key, :public_key_hash, :application_data @@ -68,6 +69,9 @@ def validate_signature(signature, ephemeral_public_key, data, transaction_id, ap verified = p7.verify([], store, verification_string, OpenSSL::PKCS7::NOVERIFY ) raise InvalidSignatureError, "The given signature is not a valid ECDSA signature." unless verified end + + # Ensure that the signing time is within "a few minutes" + raise InvalidSignatureError, "Token not signed within a few minutes" unless p7.signers.length == 1 && p7.signers.first.signed_time.between?(Time.now - SIGNATURE_VALIDITY_TIME_WINDOW, Time.now + SIGNATURE_VALIDITY_TIME_WINDOW) end def chain_of_trust_verified?(leaf_cert, intermediate_cert, root_cert) diff --git a/test/payment_token_test.rb b/test/payment_token_test.rb index cf51896..972bea7 100644 --- a/test/payment_token_test.rb +++ b/test/payment_token_test.rb @@ -2,6 +2,7 @@ require 'minitest/autorun' require 'json' require 'gala' +require 'timecop' class Gala::PaymentTokenTest < Minitest::Test @@ -14,7 +15,7 @@ def setup @merchant_id = "358DA5890B9555C0A9EFB84B5CD6FF04BFDCD5AABF5DC14B9872D8DF51EAF439" @shared_secret = Base64.decode64("yCUzDuNYTnUnANZEdxC7+DvPmqNslB2YWYn68SBsJHU=") @symmetric_key = Base64.decode64("3GTXJ4RuP/IhS23hsdOw2J2ecAZmC0RasbPIFdC3nQM=") - + @signature_timestamp = Time.new(2021, 9, 1, 19, 3, 06, "+00:00") end def test_initialize @@ -41,7 +42,10 @@ def test_symmetric_key end def test_decrypt - temp = @payment_token.decrypt(@certificate, @private_key) + temp = nil + Timecop.freeze(@signature_timestamp) do + temp = @payment_token.decrypt(@certificate, @private_key) + end payment_data = JSON.parse(temp) assert_equal "5353756319181169", payment_data["applicationPrimaryAccountNumber"] assert_equal "240930", payment_data["applicationExpirationDate"] @@ -56,8 +60,21 @@ def test_decrypt def test_failed_decrypt @payment_token.data = "bogus4OZho15e9Yp5K0EtKergKzeRpPAjnKHwmSNnagxhjwhKQ5d29sfTXjdbh1CtTJ4DYjsD6kfulNUnYmBTsruphBz7RRVI1WI8P0LrmfTnImjcq1mi" exception = assert_raises Gala::PaymentToken::InvalidSignatureError do - JSON.parse(@payment_token.decrypt(@certificate, @private_key)) + Timecop.freeze(@signature_timestamp) do + JSON.parse(@payment_token.decrypt(@certificate, @private_key)) + end end assert_equal("The given signature is not a valid ECDSA signature.", exception.message) end + + # Should fail if a signature is more than 5 minutes old + def test_failed_decrypt_replay_attack + exception = assert_raises Gala::PaymentToken::InvalidSignatureError do + Timecop.freeze(@signature_timestamp + 6 * 60) do + JSON.parse(@payment_token.decrypt(@certificate, @private_key)) + end + end + assert_equal("Token not signed within a few minutes", exception.message) + + end end