aliquot 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ba838f9284bf05a6b396005bf21c4bbecefe36432457b4b5dfb48c76997353e
4
- data.tar.gz: 96a202458e4bb76634375857f4af9c3a1f0f3529d0e2089e64b295345590778e
3
+ metadata.gz: a327bbbe5f475b924454c120979cb3bc965c5363c2740366a9f1abea19a59707
4
+ data.tar.gz: 044dd4f0c2daa2d72685133fbd27408247281449276c0e36cba1141778d98176
5
5
  SHA512:
6
- metadata.gz: 6607632c5489062965cc3a95eb66b2a572af1821f0b4506d2e5e20ea23ba974741beb6091dfc16de78b3ba53ccab62a87c16a183dbf1d4f8b6ae9b01d6bde94c
7
- data.tar.gz: d39edb966edf213869963edfaf753893a11dbe13b8f72af130be3ffb0c12da3a1dbf65b21448492500be9d63abfb8fffbfb7fdd5087dd9fbce292cd1dbe66bd0
6
+ metadata.gz: f25a55664501aa93322675512975d3471c4586112855d2d30f32b0f61fcad6240fdd3750d36e418feaa415f3a49ee752271f2d7c1dc5fe28cd6615fc5d16811b
7
+ data.tar.gz: e48a4d748adc053caa2e8a7770d5ef580fee0867497fa2d98dfac8cfc202a8183e3e8c900b56bac1cd121a95fadc4d8699511442263e93076d10e95ce53407af
@@ -12,6 +12,7 @@ module Aliquot
12
12
  # It is used to verify/decrypt the supplied token by using the shared secret,
13
13
  # thus avoiding having knowledge of merchant primary keys.
14
14
  class Payment
15
+ SUPPORTED_PROTOCOL_VERSIONS = %w[ECv1 ECv2].freeze
15
16
  ##
16
17
  # Parameters:
17
18
  # token_string:: Google Pay token (JSON string)
@@ -39,40 +40,37 @@ module Aliquot
39
40
  # Validate and decrypt the token.
40
41
  def process
41
42
  unless valid_protocol_version?
42
- raise Error, 'only ECv1 protocolVersion is supported'
43
+ raise Error, "supported protocol versions are #{SUPPORTED_PROTOCOL_VERSIONS.join(', ')}"
44
+ end
45
+
46
+ if protocol_version == 'ECv2'
47
+ @intermediate_key = validate_intermediate_key
48
+ raise InvalidSignatureError, 'intermediate certificate expired' if intermediate_key_expired?
43
49
  end
44
50
 
45
51
  check_shared_secret
46
52
 
47
- raise InvalidSignatureError unless valid_signature?
53
+ check_signature
48
54
 
49
- validator = Aliquot::Validator::SignedMessage.new(JSON.parse(@token[:signedMessage]))
50
- validator.validate
51
- signed_message = validator.output
55
+ @signed_message = validate_signed_message
52
56
 
53
57
  begin
54
- aes_key, mac_key = derive_keys(signed_message[:ephemeralPublicKey], @shared_secret, 'Google')
58
+ aes_key, mac_key = derive_keys(@signed_message[:ephemeralPublicKey], @shared_secret, 'Google')
55
59
  rescue => e
56
60
  raise KeyDerivationError, "unable to derive keys, #{e.message}"
57
61
  end
58
62
 
59
- unless self.class.valid_mac?(mac_key, signed_message[:encryptedMessage], signed_message[:tag])
60
- raise InvalidMacError
61
- end
63
+ raise InvalidMacError unless valid_mac?(mac_key)
62
64
 
63
65
  begin
64
- @message = JSON.parse(self.class.decrypt(aes_key, signed_message[:encryptedMessage]))
66
+ @message = JSON.parse(decrypt(aes_key, @signed_message[:encryptedMessage]))
65
67
  rescue JSON::JSONError => e
66
68
  raise InputError, "encryptedMessage JSON invalid, #{e.message}"
67
69
  rescue => e
68
70
  raise DecryptionError, "decryption failed, #{e.message}"
69
71
  end
70
72
 
71
- message_validator = Aliquot::Validator::EncryptedMessageValidator.new(@message)
72
- message_validator.validate
73
-
74
- # Output is hashed with symbolized keys.
75
- @message = message_validator.output
73
+ @message = validate_message
76
74
 
77
75
  raise TokenExpiredError if expired?
78
76
 
@@ -84,47 +82,161 @@ module Aliquot
84
82
  end
85
83
 
86
84
  def valid_protocol_version?
87
- protocol_version == 'ECv1'
85
+ SUPPORTED_PROTOCOL_VERSIONS.include?(protocol_version)
88
86
  end
89
87
 
90
- ##
91
- # Check if the token is expired, according to the messageExpiration included
92
- # in the token.
93
- def expired?
94
- @message[:messageExpiration].to_f / 1000.0 <= Time.now.to_f
88
+ def validate_intermediate_key
89
+ # Valid JSON as it has been checked by Token Validator.
90
+ intermediate_key = JSON.parse(@token[:intermediateSigningKey][:signedKey])
91
+
92
+ validator = Aliquot::Validator::SignedKeyValidator.new(intermediate_key)
93
+ validator.validate
94
+
95
+ validator.output
96
+ end
97
+
98
+ def intermediate_key_expired?
99
+ cur_millis = (Time.now.to_f * 1000).round
100
+ @intermediate_key[:keyExpiration].to_i < cur_millis
95
101
  end
96
102
 
97
- def valid_signature?
98
- signed_string = ['Google', @merchant_id, protocol_version, @token[:signedMessage]].map do |str|
103
+ def check_shared_secret
104
+ begin
105
+ decoded = Base64.strict_decode64(@shared_secret)
106
+ rescue
107
+ raise InvalidSharedSecretError, 'shared_secret must be base64'
108
+ end
109
+
110
+ raise InvalidSharedSecretError, 'shared_secret must be 32 bytes when base64 decoded' unless decoded.length == 32
111
+ end
112
+
113
+ def check_signature
114
+ signed_string_message = ['Google', @merchant_id, protocol_version, @token[:signedMessage]].map do |str|
99
115
  [str.length].pack('V') + str
100
116
  end.join
117
+ message_signature = Base64.strict_decode64(@token[:signature])
118
+
119
+ root_signing_keys = root_keys
120
+
121
+ case protocol_version
122
+ when 'ECv1'
123
+ # Check if signature was performed directly with any possible key.
124
+ success =
125
+ root_signing_keys.map do |key|
126
+ key.verify(new_digest, message_signature, signed_string_message)
127
+ end.any?
128
+
129
+ raise InvalidSignatureError unless success
130
+ when 'ECv2'
131
+ signed_key_signature = ['Google', 'ECv2', @token[:intermediateSigningKey][:signedKey]].map do |str|
132
+ [str.length].pack('V') + str
133
+ end.join
134
+
135
+ # Check that the intermediate key signed the message
136
+ pkey = OpenSSL::PKey::EC.new(Base64.strict_decode64(@intermediate_key[:keyValue]))
137
+ raise InvalidSignatureError, 'intermediate did not sign message' unless pkey.verify(new_digest, message_signature, signed_string_message)
138
+
139
+ intermediate_signatures = @token[:intermediateSigningKey][:signatures]
140
+
141
+ # Check that a root signing key signed the intermediate
142
+ success = valid_intermediate_key_signatures?(
143
+ root_signing_keys,
144
+ intermediate_signatures,
145
+ signed_key_signature
146
+ )
147
+
148
+ raise InvalidSignatureError, 'intermediate not signed' unless success
149
+ end
150
+ rescue OpenSSL::PKey::PKeyError => e
151
+ # Catches problems with verifying signature. Can be caused by signature
152
+ # being valid ASN1 but having invalid structure.
153
+ raise InvalidSignatureError, e.message
154
+ end
155
+
156
+ def root_keys
157
+ root_signing_keys = JSON.parse(@signing_keys)['keys'].select do |key|
158
+ key['protocolVersion'] == protocol_version
159
+ end
160
+
161
+ root_signing_keys.map! do |key|
162
+ OpenSSL::PKey::EC.new(Base64.strict_decode64(key['keyValue']))
163
+ end
164
+ end
165
+
166
+ def valid_intermediate_key_signatures?(signing_keys, signatures, signed)
167
+ signing_keys.product(signatures).each do |key, sig|
168
+ return true if key.verify(new_digest, Base64.strict_decode64(sig), signed)
169
+ end
170
+ false
171
+ end
172
+
173
+ def validate_signed_message
174
+ signed_message = @token[:signedMessage]
175
+ validator = Aliquot::Validator::SignedMessage.new(JSON.parse(signed_message))
176
+ validator.validate
177
+ validator.output
178
+ end
179
+
180
+ # Keys are derived according to the Google Pay specification.
181
+ def derive_keys(ephemeral_public_key, shared_secret, info)
182
+ input_keying_material = Base64.strict_decode64(ephemeral_public_key) + Base64.strict_decode64(shared_secret)
183
+
184
+ key_len = new_cipher.key_len
101
185
 
102
- keys = JSON.parse(@signing_keys)['keys']
103
- # Check if signature was performed with any possible key.
104
- keys.map do |key|
105
- next if key['protocolVersion'] != protocol_version
186
+ key_bytes = if OpenSSL.const_defined?(:KDF) && OpenSSL::KDF.respond_to?(:hkdf)
187
+ OpenSSL::KDF.hkdf(input_keying_material, hash: new_digest, salt: '', length: 2 * key_len, info: info)
188
+ else
189
+ HKDF.new(input_keying_material, algorithm: 'SHA256', info: info).next_bytes(2 * key_len)
190
+ end
106
191
 
107
- ec = OpenSSL::PKey::EC.new(Base64.strict_decode64(key['keyValue']))
108
- ec.verify(OpenSSL::Digest::SHA256.new, Base64.strict_decode64(@token[:signature]), signed_string)
109
- end.any?
192
+ [key_bytes[0..key_len - 1], key_bytes[key_len..2 * key_len]]
110
193
  end
111
194
 
112
- def self.decrypt(key, encrypted)
113
- c = OpenSSL::Cipher::AES128.new(:CTR)
195
+ def valid_mac?(mac_key)
196
+ data = Base64.strict_decode64(@signed_message[:encryptedMessage])
197
+ tag = @signed_message[:tag]
198
+ mac = OpenSSL::HMAC.digest(new_digest, mac_key, data)
199
+
200
+ compare(Base64.strict_encode64(mac), tag)
201
+ end
202
+
203
+ def decrypt(key, encrypted)
204
+ c = new_cipher
114
205
  c.key = key
115
206
  c.decrypt
116
207
 
117
208
  c.update(Base64.strict_decode64(encrypted)) + c.final
118
209
  end
119
210
 
120
- def self.valid_mac?(mac_key, data, tag)
121
- digest = OpenSSL::Digest::SHA256.new
122
- mac = OpenSSL::HMAC.digest(digest, mac_key, Base64.strict_decode64(data))
211
+ def validate_message
212
+ validator = Aliquot::Validator::EncryptedMessageValidator.new(@message)
213
+ validator.validate
123
214
 
124
- compare(Base64.strict_encode64(mac), tag)
215
+ # Output is hashed with symbolized keys.
216
+ validator.output
125
217
  end
126
218
 
127
- def self.compare(a, b)
219
+ ##
220
+ # Check if the token is expired, according to the messageExpiration included
221
+ # in the token.
222
+ def expired?
223
+ @message[:messageExpiration].to_f / 1000.0 <= Time.now.to_f
224
+ end
225
+
226
+ def new_cipher
227
+ case protocol_version
228
+ when 'ECv1'
229
+ OpenSSL::Cipher::AES128.new(:CTR)
230
+ when 'ECv2'
231
+ OpenSSL::Cipher::AES256.new(:CTR)
232
+ end
233
+ end
234
+
235
+ def new_digest
236
+ OpenSSL::Digest::SHA256.new
237
+ end
238
+
239
+ def compare(a, b)
128
240
  return false unless a.length == b.length
129
241
 
130
242
  diffs = 0
@@ -137,31 +249,5 @@ module Aliquot
137
249
 
138
250
  diffs.zero?
139
251
  end
140
-
141
- private
142
-
143
- # Keys are derived according to the Google Pay specification.
144
- def derive_keys(ephemeral_public_key, shared_secret, info)
145
- input_keying_material = Base64.strict_decode64(ephemeral_public_key) + Base64.strict_decode64(shared_secret)
146
-
147
- if OpenSSL.const_defined?(:KDF) && OpenSSL::KDF.respond_to?(:hkdf)
148
- h = OpenSSL::Digest::SHA256.new
149
- key_bytes = OpenSSL::KDF.hkdf(input_keying_material, hash: h, salt: '', length: 32, info: info)
150
- else
151
- key_bytes = HKDF.new(input_keying_material, algorithm: 'SHA256', info: info).next_bytes(32)
152
- end
153
-
154
- [key_bytes[0..15], key_bytes[16..32]]
155
- end
156
-
157
- def check_shared_secret
158
- begin
159
- decoded = Base64.strict_decode64(@shared_secret)
160
- rescue
161
- raise InvalidSharedSecretError, 'shared_secret must be base64'
162
- end
163
-
164
- raise InvalidSharedSecretError, 'shared_secret must be 32 bytes when base64 decoded' unless decoded.length == 32
165
- end
166
252
  end
167
253
  end
@@ -21,6 +21,7 @@ module Aliquot
21
21
  integer_string?: 'must be string encoded integer',
22
22
  month?: 'must be a month (1..12)',
23
23
  year?: 'must be a year (2000..3000)',
24
+ base64_asn1?: 'must be base64 encoded asn1 value',
24
25
 
25
26
  authMethodCryptogram3DS: 'authMethod CRYPTOGRAM_3DS requires eciIndicator',
26
27
  authMethodCard: 'eciIndicator/cryptogram must be omitted when PAN_ONLY',
@@ -61,6 +62,8 @@ module Aliquot
61
62
  predicate(:month?) { |x| x.between?(1, 12) }
62
63
 
63
64
  predicate(:year?) { |x| x.between?(2000, 3000) }
65
+
66
+ predicate(:base64_asn1?) { |x| OpenSSL::ASN1.decode(Base64.strict_decode64(x)) rescue false }
64
67
  end
65
68
 
66
69
  # Base for DRY-Validation schemas used in Aliquot.
@@ -71,13 +74,32 @@ module Aliquot
71
74
  end
72
75
  end
73
76
 
77
+ # Schema used for the 'intermediateSigningKey' hash included in ECv2.
78
+ IntermediateSigningKeySchema = Dry::Validation.Schema(BaseSchema) do
79
+ required(:signedKey).filled(:str?, :json_string?)
80
+
81
+ # TODO: Check if elements of array are valid signatures
82
+ required(:signatures).filled(:array?) { each { base64? & base64_asn1? } }
83
+ end
84
+
85
+ SignedKeySchema = Dry::Validation.Schema(BaseSchema) do
86
+ required(:keyExpiration).filled(:integer_string?)
87
+ required(:keyValue).filled(:ec_public_key?)
88
+ end
89
+
74
90
  # DRY-Validation schema for Google Pay token
75
91
  TokenSchema = Dry::Validation.Schema(BaseSchema) do
76
- required(:signature).filled(:str?, :base64?)
92
+ required(:signature).filled(:str?, :base64?, :base64_asn1?)
77
93
 
78
94
  # Currently supposed to be ECv1, but may evolve.
79
95
  required(:protocolVersion).filled(:str?)
80
96
  required(:signedMessage).filled(:str?, :json_string?)
97
+
98
+ optional(:intermediateSigningKey).schema(IntermediateSigningKeySchema)
99
+
100
+ rule('ECv2 implies intermediateSigningKey': %i[protocolVersion intermediateSigningKey]) do |version, intermediatekey|
101
+ version.eql?('ECv2') > intermediatekey.filled?
102
+ end
81
103
  end
82
104
 
83
105
  # DRY-Validation schema for signedMessage component Google Pay token
@@ -180,5 +202,14 @@ module Aliquot
180
202
  @schema = EncryptedMessageSchema
181
203
  end
182
204
  end
205
+
206
+ class SignedKeyValidator
207
+ include InstanceMethods
208
+ class Error < ::Aliquot::Error; end
209
+ def initialize(input)
210
+ @input = input
211
+ @schema = SignedKeySchema
212
+ end
213
+ end
183
214
  end
184
215
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aliquot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clearhaus
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-08 00:00:00.000000000 Z
11
+ date: 2019-01-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-validation
@@ -58,42 +58,42 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 0.6.0
61
+ version: '0.8'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 0.6.0
68
+ version: '0.8'
69
69
  - !ruby/object:Gem::Dependency
70
- name: pry
70
+ name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: '3'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: '3'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rspec
84
+ name: pry
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '3'
89
+ version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '3'
96
+ version: '0'
97
97
  description:
98
98
  email: hello@clearhaus.com
99
99
  executables: []