pedicel 0.0.4 → 0.0.5

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: 628cf5b61cec879b91695fdbbf8f9c3a0803a9c7be2645076d34da772f030b86
4
- data.tar.gz: 04dd8ff38fe186fdf5f79a2c648a9482e63add4df9ac2b42aa2b229b2c4ae3fa
3
+ metadata.gz: 67d155d5766564d4ca2580589ca57cacb94415eb7b7b3bfa9eefe01d6b30e270
4
+ data.tar.gz: a384e5e0b28d9aaa8d51fbe80dfe532efd4adfaa27ce44995306385ae0b7363f
5
5
  SHA512:
6
- metadata.gz: 111ea8c7a4baf9cdf7128aeb710cd9ea8d51a31e722d2ae93939812bc12cee9c388476e62b33042ca5fe41324239b106f87a128e2ea599b57b9d2771ec7521fe
7
- data.tar.gz: ad718264658b4c26ae9c62644e98a5b6056637b781a3100c01666e08b1d07ff569c21abe1cb7bd18647bcd904898730b4b081349552b37d81672f72242b4a9b5
6
+ metadata.gz: fda0a0d5b14ddf620e42523352b5ced7c6725d03b1a6d6af12677220e42ca979188fd3049602bc0946d2f456572f8e8c92681a42b8a28cd353834e397f06192f
7
+ data.tar.gz: 40b85a602bd41992fb8ddf8e147dac5668422b9148319ff4d8885eb7a825498a0ef638a73655453a08079f2d66426be3b13333cfa7721c0bd1e8bd3241b5bbf8
@@ -9,36 +9,37 @@ module Pedicel
9
9
  attr_reader :config
10
10
 
11
11
  def initialize(token, config: Pedicel::DEFAULT_CONFIG)
12
- Validator.validate_token(token)
12
+ validation = Validator::Token.new(token)
13
+ validation.validate
13
14
 
14
- @token = token
15
+ @token = validation.output
15
16
  @config = config
16
17
  end
17
18
 
18
19
  def version
19
- @token['version']&.to_sym
20
+ @token[:version].to_sym
20
21
  end
21
22
 
22
23
  def encrypted_data
23
- return nil unless @token['data']
24
+ return nil unless @token[:data]
24
25
 
25
- Base64.decode64(@token['data'])
26
+ Base64.decode64(@token[:data])
26
27
  end
27
28
 
28
29
  def signature
29
- return nil unless @token['signature']
30
+ return nil unless @token[:signature]
30
31
 
31
- Base64.decode64(@token['signature'])
32
+ Base64.decode64(@token[:signature])
32
33
  end
33
34
 
34
35
  def transaction_id
35
- [@token['header']['transactionId']].pack('H*')
36
+ [@token[:header][:transactionId]].pack('H*')
36
37
  end
37
38
 
38
39
  def application_data
39
- return nil unless @token['header']['applicationData']
40
+ return nil unless @token[:header][:applicationData]
40
41
 
41
- [@token['header']['applicationData']].pack('H*')
42
+ [@token[:header][:applicationData]].pack('H*')
42
43
  end
43
44
 
44
45
  def private_key_class
@@ -65,8 +66,7 @@ module Pedicel
65
66
  end
66
67
  end
67
68
 
68
- private
69
- def decrypt_aes_openssl(key)
69
+ private def decrypt_aes_openssl(key)
70
70
  cipher = OpenSSL::Cipher.new(symmetric_algorithm)
71
71
  cipher.decrypt
72
72
 
@@ -93,14 +93,13 @@ module Pedicel
93
93
  raise Pedicel::AesKeyError, 'wrong key'
94
94
  end
95
95
 
96
- def decrypt_aes_gem(key)
96
+ private def decrypt_aes_gem(key)
97
97
  require 'aes256gcm_decrypt'
98
98
 
99
99
  Aes256GcmDecrypt.decrypt(encrypted_data, key)
100
100
  rescue Aes256GcmDecrypt::Error => e
101
101
  raise Pedicel::AesKeyError, "decryption failed: #{e}"
102
102
  end
103
- public
104
103
 
105
104
  def valid_signature?(now: Time.now)
106
105
  !!verify_signature(now: now)
@@ -126,7 +125,9 @@ module Pedicel
126
125
  # 1.a
127
126
  # Ensure that the certificates contain the correct custom OIDs: (...).
128
127
  # The value for these marker OIDs doesn't matter, only their presence.
129
- leaf, intermediate, other = self.class.extract_certificates(signature: s)
128
+ leaf, intermediate, other = self.class.extract_certificates(signature: s,
129
+ intermediate_oid: @config[:oid_intermediate_certificate],
130
+ leaf_oid: @config[:oid_leaf_certificate])
130
131
  # Implicit since these are the ones extracted.
131
132
 
132
133
  # 1.b
@@ -154,12 +155,14 @@ module Pedicel
154
155
 
155
156
  # 1.e
156
157
  # Inspect the CMS signing time of the signature (...)
157
- self.class.verify_signed_time(signature: s, now: now)
158
+ self.class.verify_signed_time(signature: s, now: now, few_min: @config[:replay_threshold_seconds])
158
159
 
159
160
  self
160
161
  end
161
162
 
162
- def self.extract_certificates(signature:, config: Pedicel::DEFAULT_CONFIG)
163
+ def self.extract_certificates(signature:,
164
+ intermediate_oid: Pedicel::DEFAULT_CONFIG[:oid_intermediate_certificate],
165
+ leaf_oid: Pedicel::DEFAULT_CONFIG[:oid_leaf_certificate])
163
166
  leafs, intermediates, others = [], [], []
164
167
 
165
168
  signature.certificates.each do |certificate|
@@ -167,10 +170,10 @@ module Pedicel
167
170
 
168
171
  certificate.extensions.each do |extension|
169
172
  case extension.oid
170
- when config[:oid_intermediate_certificate]
173
+ when intermediate_oid
171
174
  intermediates << certificate
172
175
  leaf_or_intermediate = true
173
- when config[:oid_leaf_certificate]
176
+ when leaf_oid
174
177
  leafs << certificate
175
178
  leaf_or_intermediate = true
176
179
  end
@@ -179,8 +182,8 @@ module Pedicel
179
182
  others << certificate unless leaf_or_intermediate
180
183
  end
181
184
 
182
- raise SignatureError, "no unique leaf certificate found (OID #{config[:oid_leaf_certificate]})" unless leafs.length == 1
183
- raise SignatureError, "no unique intermediate certificate found (OID #{config[:oid_intermediate_certificate]})" unless intermediates.length == 1
185
+ raise SignatureError, "no unique leaf certificate found (OID #{leaf_oid})" unless leafs.length == 1
186
+ raise SignatureError, "no unique intermediate certificate found (OID #{intermediate_oid})" unless intermediates.length == 1
184
187
  raise SignatureError, "too many certificates found in the signature: #{others.map(&:subject).join('; ')}" if others.length > 1
185
188
 
186
189
  [leafs.first, intermediates.first, others.first]
@@ -222,7 +225,7 @@ module Pedicel
222
225
  true
223
226
  end
224
227
 
225
- def self.verify_signed_time(signature:, now:, config: Pedicel::DEFAULT_CONFIG)
228
+ def self.verify_signed_time(signature:, now: Time.now, few_min: Pedicel::DEFAULT_CONFIG[:replay_threshold_seconds])
226
229
  # Inspect the CMS signing time of the signature, as defined by section
227
230
  # 11.3 of RFC 5652. If the time signature and the transaction time differ
228
231
  # by more than a few minutes, it's possible that the token is a replay
@@ -234,8 +237,6 @@ module Pedicel
234
237
  end
235
238
  signed_time = signature.signers.first.signed_time
236
239
 
237
- few_min = config[:replay_threshold_seconds]
238
-
239
240
  # Time objects. DST aware. Ignoring leap seconds. Both ends included.
240
241
  return true if signed_time.between?(now - few_min, now + few_min)
241
242
 
@@ -3,7 +3,7 @@ require 'pedicel/base'
3
3
  module Pedicel
4
4
  class EC < Base
5
5
  def ephemeral_public_key
6
- Base64.decode64(@token['header']['ephemeralPublicKey'])
6
+ Base64.decode64(@token[:header][:ephemeralPublicKey])
7
7
  end
8
8
 
9
9
  def decrypt(symmetric_key: nil, merchant_id: nil, certificate: nil, private_key: nil,
@@ -40,7 +40,7 @@ module Pedicel
40
40
 
41
41
  shared_secret = shared_secret(private_key: private_key)
42
42
 
43
- merchant_id ||= self.class.merchant_id(certificate: certificate)
43
+ merchant_id ||= self.class.merchant_id(certificate: certificate, mid_oid: @config[:oid_merchant_identifier_field])
44
44
 
45
45
  self.class.symmetric_key(shared_secret: shared_secret, merchant_id: merchant_id)
46
46
  end
@@ -111,7 +111,7 @@ module Pedicel
111
111
  sha256.digest
112
112
  end
113
113
 
114
- def self.merchant_id(certificate:, config: Pedicel::DEFAULT_CONFIG)
114
+ def self.merchant_id(certificate:, mid_oid: Pedicel::DEFAULT_CONFIG[:oid_merchant_identifier_field])
115
115
  begin
116
116
  cert = OpenSSL::X509::Certificate.new(certificate)
117
117
  rescue => e
@@ -121,7 +121,7 @@ module Pedicel
121
121
  merchant_id_hex =
122
122
  cert
123
123
  .extensions
124
- .find { |x| x.oid == config[:oid_merchant_identifier_field] }
124
+ .find { |x| x.oid == mid_oid }
125
125
  &.value # Hex encoded Merchant ID plus perhaps extra non-hex chars.
126
126
  &.delete('^[0-9a-fA-F]') # Remove non-hex chars.
127
127
 
@@ -3,31 +3,30 @@ require 'base64'
3
3
  require 'openssl'
4
4
 
5
5
  module Pedicel
6
- # Validation class for Apple Pay Payment Token and associated data:
6
+
7
+ # Validations for Apple Pay Payment Token and associated data:
7
8
  # https://developer.apple.com/library/content/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html
8
9
  # This purposefully only does syntactic validation (as opposed to semantic).
9
- class Validator
10
- class Error < StandardError; end
11
- class TokenFormatError < Error; end
12
- class TokenDataFormatError < Error; end
10
+ module Validator
13
11
 
14
12
  module Predicates
15
13
  include Dry::Logic::Predicates
16
14
 
17
15
  CUSTOM_PREDICATE_ERRORS = {
18
- base64?: 'invalid base64',
19
- hex?: 'invalid hex',
20
- pan?: 'invalid pan',
21
- yymmdd?: 'invalid date format YYMMDD',
22
- ec_public_key?: 'is not an EC public key',
23
- pkcs7_signature?: 'is not a PKCS7 Signature',
24
- eci?: 'not an ECI indicator',
25
- hex_sha256?: 'not hex-encoded SHA256',
26
- base64_sha256?: 'not base64-encoded SHA256',
16
+ base64?: 'must be Base64',
17
+ hex?: 'must be hex',
18
+ pan?: 'must be a pan',
19
+ yymmdd?: 'must be formatted YYMMDD',
20
+ ec_public_key?: 'must be an EC public key',
21
+ pkcs7_signature?: 'must be a PKCS7 Signature',
22
+ eci?: 'must be an ECI',
23
+ hex_sha256?: 'must be a hex-encoded SHA-256',
24
+ base64_sha256?: 'must be a Base64-encoded SHA-256',
25
+ iso4217_numeric?: 'must be an ISO 4217 numeric code',
27
26
  }.freeze
28
27
 
29
28
  # Support Ruby 2.3, but use the faster #match? when available.
30
- match_b = String.new.respond_to?(:match?) ? lambda{|s, re| s.match?(re)} : lambda{|s, re| !!(s =~ re)}
29
+ match_b = String.new.respond_to?(:match?) ? ->(s, re) { s.match?(re) } : ->(s, re) { !!(s =~ re) }
31
30
 
32
31
  predicate(:base64?) do |x|
33
32
  str?(x) &&
@@ -38,7 +37,7 @@ module Pedicel
38
37
  end
39
38
 
40
39
  # We should figure out how strict we should be. Hopefully we can discard
41
- # the above base64? predicate and use the following simpler one:
40
+ # the above Base64? predicate and use the following simpler one:
42
41
  #predicate(:strict_base64?) { |x| !!Base64.strict_decode64(x) rescue false }
43
42
 
44
43
  predicate(:base64_sha256?) { |x| base64?(x) && Base64.decode64(x).length == 32 }
@@ -56,113 +55,137 @@ module Pedicel
56
55
  predicate(:ec_public_key?) { |x| base64?(x) && OpenSSL::PKey::EC.new(Base64.decode64(x)).check_key rescue false }
57
56
 
58
57
  predicate(:pkcs7_signature?) { |x| base64?(x) && !!OpenSSL::PKCS7.new(Base64.decode64(x)) rescue false }
58
+
59
+ predicate(:iso4217_numeric?) { |x| match_b.(x, /\A[0-9]{3}\z/) }
59
60
  end
60
61
 
61
- TokenSchema = Dry::Validation.Schema do
62
- configure do
63
- # NOTE: This option removes/sanitizes hash element not mentioned/tested.
64
- # Hurray for good documentation.
65
- config.input_processor = :json
66
-
67
- # In theory, I would guess that :strict below would cause a failure if
68
- # untested keys were encountered, however this appears to not be the
69
- # case. Anyways, it's (of course) not documented.
70
- # config.hash_type = :strict
71
-
72
- predicates(Predicates)
73
- def self.messages
74
- super.merge(en: { errors: Predicates::CUSTOM_PREDICATE_ERRORS })
75
- end
62
+ class BaseSchema < Dry::Validation::Schema::JSON
63
+ predicates(Predicates)
64
+ def self.messages
65
+ super.merge(en: { errors: Predicates::CUSTOM_PREDICATE_ERRORS })
76
66
  end
67
+ end
77
68
 
78
- required(:data).filled(:str?, :base64?)
69
+ TokenHeaderSchema = Dry::Validation.Schema(BaseSchema) do
70
+ optional(:applicationData).filled(:str?, :hex?, :hex_sha256?)
79
71
 
80
- required(:header).schema do
81
- optional(:applicationData).filled(:str?, :hex?, :hex_sha256?)
72
+ optional(:ephemeralPublicKey).filled(:str?, :base64?, :ec_public_key?)
82
73
 
83
- optional(:ephemeralPublicKey).filled(:str?, :base64?, :ec_public_key?)
74
+ optional(:wrappedKey).filled(:str?, :base64?)
84
75
 
85
- optional(:wrappedKey).filled(:str?, :base64?)
76
+ rule('ephemeralPublicKey xor wrappedKey': [:ephemeralPublicKey, :wrappedKey]) do |e, w|
77
+ e.filled? ^ w.filled?
78
+ end
86
79
 
87
- rule('ephemeralPublicKey xor wrappedKey': [:ephemeralPublicKey, :wrappedKey]) do |e, w|
88
- e.filled? ^ w.filled?
89
- end
80
+ required(:publicKeyHash).filled(:str?, :base64?, :base64_sha256?)
90
81
 
91
- required(:publicKeyHash).filled(:str?, :base64?, :base64_sha256?)
82
+ required(:transactionId).filled(:str?, :hex?)
83
+ end
92
84
 
93
- required(:transactionId).filled(:str?, :hex?)
94
- end
85
+ TokenSchema = Dry::Validation.Schema(BaseSchema) do
86
+ required(:data).filled(:str?, :base64?)
87
+
88
+ required(:header).schema(TokenHeaderSchema)
95
89
 
96
90
  required(:signature).filled(:str?, :base64?, :pkcs7_signature?)
97
91
 
98
92
  required(:version).filled(:str?, included_in?: %w[EC_v1 RSA_v1])
99
93
  end
100
94
 
101
- TokenDataSchema = Dry::Validation.Schema do
102
- configure do
103
- predicates(Predicates)
104
- def self.messages
105
- super.merge(en: { errors: Predicates::CUSTOM_PREDICATE_ERRORS })
106
- end
107
- end
95
+ TokenDataPaymentDataSchema = Dry::Validation.Schema(BaseSchema) do
96
+ optional(:onlinePaymentCryptogram).filled(:str?, :base64?)
97
+ optional(:eciIndicator).filled(:str?, :eci?)
108
98
 
109
- required('applicationPrimaryAccountNumber').filled(:str?, :pan?)
99
+ optional(:emvData).filled(:str?, :base64?)
100
+ optional(:encryptedPINData).filled(:str?, :hex?)
101
+ end
110
102
 
111
- required('applicationExpirationDate').filled(:str?, :yymmdd?)
103
+ TokenDataSchema = Dry::Validation.Schema(BaseSchema) do
104
+ required(:applicationPrimaryAccountNumber).filled(:str?, :pan?)
112
105
 
113
- required('currencyCode').filled(:str?, format?: /\A[0-9]{3}\z/)
106
+ required(:applicationExpirationDate).filled(:str?, :yymmdd?)
114
107
 
115
- required('transactionAmount').filled(:int?)
108
+ required(:currencyCode).filled(:str?, :iso4217_numeric?)
116
109
 
117
- optional('cardholderName').filled(:str?)
110
+ required(:transactionAmount).filled(:int?)
118
111
 
119
- required('deviceManufacturerIdentifier').filled(:str?, :hex?)
112
+ optional(:cardholderName).filled(:str?)
120
113
 
121
- required('paymentDataType').filled(:str?, included_in?: %w[3DSecure EMV])
114
+ required(:deviceManufacturerIdentifier).filled(:str?, :hex?)
122
115
 
123
- required('paymentData').schema do
124
- optional('onlinePaymentCryptogram').filled(:str?, :base64?)
125
- optional('eciIndicator').filled(:str?, :eci?)
116
+ required(:paymentDataType).filled(:str?, included_in?: %w[3DSecure EMV])
126
117
 
127
- optional('emvData').filled(:str?, :base64?)
128
- optional('encryptedPINData').filled(:str?, :hex?)
118
+ required(:paymentData).schema(TokenDataPaymentDataSchema)
119
+
120
+ rule('when paymentDataType is 3DSecure, onlinePaymentCryptogram': [:paymentDataType, [:paymentData, :onlinePaymentCryptogram]]) do |type, cryptogram|
121
+ type.eql?('3DSecure') > cryptogram.filled?
122
+ end
123
+ rule('when paymentDataType is 3DSecure, emvData': [:paymentDataType, [:paymentData, :emvData]]) do |type, emv|
124
+ type.eql?('3DSecure') > emv.none?
125
+ end
126
+ rule('when paymentDataType is 3DSecure, encryptedPINData': [:paymentDataType, [:paymentData, :encryptedPINData]]) do |type, pin|
127
+ type.eql?('3DSecure') > pin.none?
129
128
  end
130
129
 
131
- rule('paymentDataType affects paymentData': [:paymentDataType, [:paymentData, :onlinePaymentCryptogram]]) do |t, cryptogram|
132
- t.eql?('3DSecure') > cryptogram.filled?
130
+ rule('when paymentDataType is EMV, onlinePaymentCryptogram': [:paymentDataType, [:paymentData, :onlinePaymentCryptogram]]) do |type, cryptogram|
131
+ type.eql?('EMV') > cryptogram.none?
133
132
  end
133
+ rule('when paymentDataType is EMV, eciIndicator': [:paymentDataType, [:paymentData, :eciIndicator]]) do |type, eci|
134
+ type.eql?('EMV') > eci.none?
135
+ end
136
+ rule('when paymentDataType is EMV, emvData': [:paymentDataType, [:paymentData, :emvData]]) do |type, emv|
137
+ type.eql?('EMV') > emv.filled?
138
+ end
139
+ rule('when paymentDataType is EMV, encryptedPINData': [:paymentDataType, [:paymentData, :encryptedPINData]]) do |type, pin|
140
+ type.eql?('EMV') > pin.filled?
141
+ end
142
+
134
143
  end
135
144
 
136
- def self.validate_token(token)
137
- validation = TokenSchema.call(token)
145
+ class Error < StandardError; end
138
146
 
139
- raise TokenFormatError, format_errors(validation) if validation.failure?
147
+ module InstanceMethods
148
+ attr_reader :output
140
149
 
141
- true
142
- end
150
+ def validate
151
+ @validation ||= @schema.call(@input)
143
152
 
144
- def self.valid_token?(token)
145
- validate_token(token)
146
- rescue TokenFormatError
147
- false
148
- end
153
+ @output = @validation.output
149
154
 
150
- def self.validate_token_data(token_data)
151
- validation = TokenDataSchema.call(token_data)
155
+ return true if @validation.success?
152
156
 
153
- raise TokenDataFormatError, format_errors(validation) if validation.failure?
157
+ raise Error, "validation error: #{@validation.errors.keys.join(', ')}"
158
+ end
159
+
160
+ def valid?
161
+ validate
162
+ rescue Error
163
+ false
164
+ end
165
+
166
+ def errors
167
+ valid? unless @validation
154
168
 
155
- true
169
+ @validation.errors
170
+ end
156
171
  end
157
172
 
158
- def self.valid_token_data?(token_data)
159
- validate_token_data(token_data)
160
- rescue TokenDataFormatError
161
- false
173
+ class Token
174
+ include InstanceMethods
175
+ class Error < ::Pedicel::Validator::Error; end
176
+ def initialize(input)
177
+ @input = input
178
+ @schema = TokenSchema
179
+ end
162
180
  end
163
181
 
164
- def self.format_errors(validation)
165
- validation.errors.map{|key, msg| "#{key}: #{msg}"}.join('; ')
182
+ class TokenData
183
+ include InstanceMethods
184
+ class Error < ::Pedicel::Validator::Error; end
185
+ def initialize(input)
186
+ @input = input
187
+ @schema = TokenDataSchema
188
+ end
166
189
  end
167
190
  end
168
191
  end
@@ -1,3 +1,3 @@
1
1
  module Pedicel
2
- VERSION = '0.0.4'.freeze
2
+ VERSION = '0.0.5'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pedicel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clearhaus
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-04 00:00:00.000000000 Z
11
+ date: 2018-06-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-validation