pedicel 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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