Skip to content

Commit

Permalink
Intermidiate commit, squash/reword later
Browse files Browse the repository at this point in the history
  • Loading branch information
brorbw committed Jan 12, 2024
1 parent 3152cec commit 75a06a5
Show file tree
Hide file tree
Showing 5 changed files with 505 additions and 341 deletions.
2 changes: 2 additions & 0 deletions lib/aliquot/payment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def process

begin
@message = JSON.parse(decrypt(aes_key, @signed_message[:encryptedMessage]))
@message.merge!('threedsCryptogram' => @message.delete('3dsCryptogram')) if @message['3dsCryptogram']
@message
rescue JSON::JSONError => e
raise InputError, "encryptedMessage JSON is invalid, #{e.message}"
rescue => e
Expand Down
86 changes: 63 additions & 23 deletions lib/aliquot/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ module Validator
base64_asn1?: 'must be base64-encoded ANS.1 value',
json?: 'must be valid JSON',

is_authMethodCryptogram3DS: 'authMethod CRYPTOGRAM_3DS requires eciIndicator',
is_authMethodCard: 'eciIndicator/cryptogram must be omitted when PAN_ONLY',
is_authMethodCryptogram3DS: 'authMethod CRYPTOGRAM_3DS or 3DS requires eciIndicator',
is_authMethodCard: 'eciIndicator/cryptogram/3dsCryptogram must be omitted when PAN_ONLY',
}.freeze

def self.base64_check(value)
Expand Down Expand Up @@ -168,52 +168,92 @@ class SignedMessageContract < Dry::Validation::Contract

SignedMessageSchema = SignedMessageContract.new

# DRY-Validation schema for paymentMethodDetails component Google Pay token
class PaymentMethodDetailsContract < Dry::Validation::Contract
class CommonPaymentMethodDetailsContract < Dry::Validation::Contract
json do
required(:pan).filled(:str?)
required(:expirationMonth).filled(:int?)
required(:expirationYear).filled(:int?)
required(:authMethod).filled(:str?, included_in?: %w[PAN_ONLY CRYPTOGRAM_3DS])

optional(:cryptogram).filled(:str?)
optional(:eciIndicator).filled(:str?)
end
rule(:pan).validate(:integer_string?, :pan?)
rule(:expirationMonth).validate(:month?)
rule(:expirationYear).validate(:year?)
rule(:eciIndicator).validate(:eci?)
end

class ECv1_PaymentMethodDetailsContract < Dry::Validation::Contract
json(CommonPaymentMethodDetailsContract.schema) do
required(:pan).filled(:str?)
end

rule(:pan).validate(:integer_string?, :pan?)
end

rule(:cryptogram) do
key.failure('is missing') if 'CRYPTOGRAM_3DS'.eql?(values[:authMethod]) &&
values[:cryptogram].nil?
class ECv1_TokenizedPaymentMethodDetailsContract < Dry::Validation::Contract
json(CommonPaymentMethodDetailsContract.schema) do
required(:dpan).filled(:str?)
required(:threedsCryptogram).filled(:str?)
required(:eciIndicator).filled(:str?)
required(:authMethod).filled(:str?, included_in?: %w[3DS])
end

rule(:cryptogram) do
key.failure('cannot be defined') if 'PAN_ONLY'.eql?(values[:authMethod]) &&
!values[:cryptogram].nil?
rule(:dpan).validate(:integer_string?, :pan?)
rule(:eciIndicator).validate(:eci?)
end

class ECv2_PaymentMethodDetailsContract < Dry::Validation::Contract
json(CommonPaymentMethodDetailsContract.schema) do
required(:pan).filled(:str?)
required(:authMethod).filled(:str?, included_in?: %w[PAN_ONLY])
end

rule(:eciIndicator) do
key.failure('cannot be defined') if 'PAN_ONLY'.eql?(values[:authMethod]) &&
!values[:eciIndicator].nil?
rule(:pan).validate(:integer_string?, :pan?)
end

class ECv2_TokenizedPaymentMethodDetailsContract < Dry::Validation::Contract
json(CommonPaymentMethodDetailsContract.schema) do
required(:pan).filled(:str?)
required(:cryptogram).filled(:str?)
required(:eciIndicator).filled(:str?)
required(:authMethod).filled(:str?, included_in?: %w[CRYPTOGRAM_3DS])
end

rule(:pan).validate(:integer_string?, :pan?)
rule(:eciIndicator).validate(:eci?)
end

PaymentMethodDetailsSchema = PaymentMethodDetailsContract.new
# PaymentMethodDetailsSchema = PaymentMethodDetailsContract.new

# DRY-Validation schema for encryptedMessage component Google Pay token
class EncryptedMessageContract < Dry::Validation::Contract
json do
required(:messageExpiration).filled(:str?)
required(:messageId).filled(:str?)
required(:paymentMethod).filled(:str?)
required(:paymentMethodDetails).filled(:hash).schema(PaymentMethodDetailsContract.schema)
required(:paymentMethodDetails).filled(:hash)
optional(:gatewayMerchantId).filled(:str?)
end
rule(:messageExpiration).validate(:integer_string?)
rule(:paymentMethodDetails) do
contract = nil
if 'ECv1'.eql?(values[:protocolVersion])
if 'TOKENIZED_CARD'.eql?(values[:paymentMethod])
contract = ECv1_TokenizedPaymentMethodDetailsContract.new
else
contract = ECv1_PaymentMethodDetailsContract.new
end
else
if 'CRYPTOGRAM_3DS'.eql?(values[:authMethod])
contract = ECv2_TokenizedPaymentMethodDetailsContract.new
else
contract = ECv2_PaymentMethodDetailsContract.new
end
key.failure("test") unless
contract.(values)
end
end
rule(:paymentMethod) do
key.failure('must be equal to CARD') unless 'CARD'.eql?(value)
if '3DS'.eql?(values[:paymentMethodDetails] && values[:paymentMethodDetails]['authMethod']) # Tokenized ECv1
key.failure('must be equal to TOKENIZED_CARD') unless 'TOKENIZED_CARD'.eql?(value)
else
key.failure('must be equal to CARD') unless 'CARD'.eql?(value)
end
end
end

Expand Down
4 changes: 2 additions & 2 deletions spec/integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@
end

context :ECv1 do
let (:generator) { AliquotPay.new(:ECv1) }
let (:generator) { AliquotPay.new(protocol_version: :ECv1, type: :browser) }

include_examples 'common integration tests'
end

context :ECv2 do
let (:generator) { AliquotPay.new(:ECv2) }
let (:generator) { AliquotPay.new(protocol_version: :ECv2, type: :browser) }

include_examples 'common integration tests'

Expand Down
113 changes: 70 additions & 43 deletions spec/lib/aliquot/payment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,7 @@
# Test that all `raise` errors are caught and handled gracefully.
# Ordered by occurrence of raise in the `payment.rb` source code.

shared_examples Aliquot::Payment do

it 'decrypts' do
expect { subject.call }.to_not raise_error
expect(subject.call[:paymentMethodDetails]).to include(authMethod: 'PAN_ONLY')
end

it 'decrypts with CRYPTOGRAM_3DS' do
generator.auth_method = 'CRYPTOGRAM_3DS'
expect { subject.call }.to_not raise_error
expect(subject.call[:paymentMethodDetails]).to include(authMethod: 'CRYPTOGRAM_3DS')
end

shared_examples 'all protocol versions' do
# CB: Not sure how to trigger this test as JSON.parse has changed since 2.3
# see https://clearhaus.slack.com/archives/C3LG75WE9/p1661940442665459
it 'rejects invalid token JSON gracefully'
Expand Down Expand Up @@ -57,9 +45,9 @@
it 'rejects non-base64 shared_secret' do
block = proc do
Aliquot::Payment.new(generator.token.to_json,
'not base64',
generator.recipient_id,
signing_keys: generator.extract_root_signing_keys)
'not base64',
generator.recipient_id,
signing_keys: generator.extract_root_signing_keys)
.process
end
expect(&block).to raise_error(Aliquot::InvalidSharedSecretError, 'shared_secret must be base64')
Expand All @@ -85,47 +73,86 @@
end
end

shared_examples 'only ECv2' do
it 'rejects expired intermediateSigningKey' do
generator.key_expiration = "#{Time.now.to_i - 1}000"
expect { subject.call }.to raise_error(Aliquot::InvalidSignatureError, 'intermediate certificate is expired')
end

it 'rejects when no signature of intermediateKey is found' do
generator.signatures = AliquotPay.new.build_signatures
expect { subject.call }.to raise_error(Aliquot::InvalidSignatureError, 'no valid signature of intermediate key')
end

it 'allows invalid intermediate signatures to be present' do
fake_signature = AliquotPay.new.build_signatures.first
real_signature = generator.build_signatures.first

generator.signatures = [fake_signature, real_signature]

expect(token['intermediateSigningKey']['signatures']).to include(fake_signature)
expect(token['intermediateSigningKey']['signatures']).to include(real_signature)

expect { subject.call }.to_not raise_error
end
end

describe Aliquot::Payment do
let(:generator) { AliquotPay.new(protocol_version: :ECv1, type: :browser) }
subject do
-> do Aliquot::Payment.new(generator.token.to_json,
generator.shared_secret,
generator.recipient_id,
signing_keys: generator.extract_root_signing_keys)
.process
.process
end
end

let(:token) { generator.token }

context 'ECv1' do
let(:generator) { AliquotPay.new(:ECv1) }
include_examples Aliquot::Payment
context 'non-tokenized' do
let(:generator) { AliquotPay.new(protocol_version: :ECv1, type: :browser) }
let(:token) { generator.token }

include_examples 'all protocol versions'
it 'decrypts with PAN_ONLY' do
expect { subject.call }.to_not raise_error
expect(subject.call[:paymentMethodDetails]).to_not include('authMethod' => 'PAN_ONLY')
end
end
context 'tokenized' do
let(:generator) { AliquotPay.new(protocol_version: :ECv1, type: :app) }
let(:token) { generator.token }

include_examples 'all protocol versions'
it 'decrypts with 3DS' do
expect { subject.call }.to_not raise_error
expect(subject.call[:paymentMethodDetails]).to include('authMethod' => '3DS')
end
end
end

context 'ECv2' do
let(:generator) { AliquotPay.new(:ECv2) }
include_examples Aliquot::Payment

it 'rejects expired intermediateSigningKey' do
generator.key_expiration = "#{Time.now.to_i - 1}000"
expect { subject.call }.to raise_error(Aliquot::InvalidSignatureError, 'intermediate certificate is expired')
end

it 'rejects when no signature of intermediateKey is found' do
generator.signatures = AliquotPay.new.build_signatures
expect { subject.call }.to raise_error(Aliquot::InvalidSignatureError, 'no valid signature of intermediate key')
context 'non-tokenized' do
let(:generator) { AliquotPay.new(protocol_version: :ECv2, type: :browser) }
let(:token) { generator.token }

include_examples 'all protocol versions'
include_examples 'only ECv2'
it 'decrypts' do
expect { subject.call }.to_not raise_error
expect(subject.call[:paymentMethodDetails]).to include('authMethod' => 'PAN_ONLY')
end
end

it 'allows invalid intermediate signatures to be present' do
fake_signature = AliquotPay.new.build_signatures.first
real_signature = generator.build_signatures.first

generator.signatures = [fake_signature, real_signature]

expect(token['intermediateSigningKey']['signatures']).to include(fake_signature)
expect(token['intermediateSigningKey']['signatures']).to include(real_signature)

expect { subject.call }.to_not raise_error
context 'tokenized' do
let(:generator) { AliquotPay.new(protocol_version: :ECv2, type: :app) }
let(:token) { generator.token }

include_examples 'all protocol versions'
include_examples 'only ECv2'
it 'decrypts' do
expect { subject.call }.to_not raise_error
expect(subject.call[:paymentMethodDetails]).to include('authMethod' => 'CRYPTOGRAM_3DS')
end
end
end
end
Loading

0 comments on commit 75a06a5

Please sign in to comment.