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 +4 -4
- data/lib/pedicel/base.rb +25 -24
- data/lib/pedicel/ec.rb +4 -4
- data/lib/pedicel/validator.rb +106 -83
- data/lib/pedicel/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67d155d5766564d4ca2580589ca57cacb94415eb7b7b3bfa9eefe01d6b30e270
|
4
|
+
data.tar.gz: a384e5e0b28d9aaa8d51fbe80dfe532efd4adfaa27ce44995306385ae0b7363f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fda0a0d5b14ddf620e42523352b5ced7c6725d03b1a6d6af12677220e42ca979188fd3049602bc0946d2f456572f8e8c92681a42b8a28cd353834e397f06192f
|
7
|
+
data.tar.gz: 40b85a602bd41992fb8ddf8e147dac5668422b9148319ff4d8885eb7a825498a0ef638a73655453a08079f2d66426be3b13333cfa7721c0bd1e8bd3241b5bbf8
|
data/lib/pedicel/base.rb
CHANGED
@@ -9,36 +9,37 @@ module Pedicel
|
|
9
9
|
attr_reader :config
|
10
10
|
|
11
11
|
def initialize(token, config: Pedicel::DEFAULT_CONFIG)
|
12
|
-
Validator.
|
12
|
+
validation = Validator::Token.new(token)
|
13
|
+
validation.validate
|
13
14
|
|
14
|
-
@token =
|
15
|
+
@token = validation.output
|
15
16
|
@config = config
|
16
17
|
end
|
17
18
|
|
18
19
|
def version
|
19
|
-
@token[
|
20
|
+
@token[:version].to_sym
|
20
21
|
end
|
21
22
|
|
22
23
|
def encrypted_data
|
23
|
-
return nil unless @token[
|
24
|
+
return nil unless @token[:data]
|
24
25
|
|
25
|
-
Base64.decode64(@token[
|
26
|
+
Base64.decode64(@token[:data])
|
26
27
|
end
|
27
28
|
|
28
29
|
def signature
|
29
|
-
return nil unless @token[
|
30
|
+
return nil unless @token[:signature]
|
30
31
|
|
31
|
-
Base64.decode64(@token[
|
32
|
+
Base64.decode64(@token[:signature])
|
32
33
|
end
|
33
34
|
|
34
35
|
def transaction_id
|
35
|
-
[@token[
|
36
|
+
[@token[:header][:transactionId]].pack('H*')
|
36
37
|
end
|
37
38
|
|
38
39
|
def application_data
|
39
|
-
return nil unless @token[
|
40
|
+
return nil unless @token[:header][:applicationData]
|
40
41
|
|
41
|
-
[@token[
|
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:,
|
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
|
173
|
+
when intermediate_oid
|
171
174
|
intermediates << certificate
|
172
175
|
leaf_or_intermediate = true
|
173
|
-
when
|
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 #{
|
183
|
-
raise SignatureError, "no unique intermediate certificate found (OID #{
|
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
|
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
|
|
data/lib/pedicel/ec.rb
CHANGED
@@ -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[
|
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:,
|
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 ==
|
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
|
|
data/lib/pedicel/validator.rb
CHANGED
@@ -3,31 +3,30 @@ require 'base64'
|
|
3
3
|
require 'openssl'
|
4
4
|
|
5
5
|
module Pedicel
|
6
|
-
|
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
|
-
|
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?: '
|
19
|
-
hex?: '
|
20
|
-
pan?: '
|
21
|
-
yymmdd?: '
|
22
|
-
ec_public_key?: '
|
23
|
-
pkcs7_signature?: '
|
24
|
-
eci?: '
|
25
|
-
hex_sha256?: '
|
26
|
-
base64_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?) ?
|
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
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
69
|
+
TokenHeaderSchema = Dry::Validation.Schema(BaseSchema) do
|
70
|
+
optional(:applicationData).filled(:str?, :hex?, :hex_sha256?)
|
79
71
|
|
80
|
-
|
81
|
-
optional(:applicationData).filled(:str?, :hex?, :hex_sha256?)
|
72
|
+
optional(:ephemeralPublicKey).filled(:str?, :base64?, :ec_public_key?)
|
82
73
|
|
83
|
-
|
74
|
+
optional(:wrappedKey).filled(:str?, :base64?)
|
84
75
|
|
85
|
-
|
76
|
+
rule('ephemeralPublicKey xor wrappedKey': [:ephemeralPublicKey, :wrappedKey]) do |e, w|
|
77
|
+
e.filled? ^ w.filled?
|
78
|
+
end
|
86
79
|
|
87
|
-
|
88
|
-
e.filled? ^ w.filled?
|
89
|
-
end
|
80
|
+
required(:publicKeyHash).filled(:str?, :base64?, :base64_sha256?)
|
90
81
|
|
91
|
-
|
82
|
+
required(:transactionId).filled(:str?, :hex?)
|
83
|
+
end
|
92
84
|
|
93
|
-
|
94
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
99
|
+
optional(:emvData).filled(:str?, :base64?)
|
100
|
+
optional(:encryptedPINData).filled(:str?, :hex?)
|
101
|
+
end
|
110
102
|
|
111
|
-
|
103
|
+
TokenDataSchema = Dry::Validation.Schema(BaseSchema) do
|
104
|
+
required(:applicationPrimaryAccountNumber).filled(:str?, :pan?)
|
112
105
|
|
113
|
-
required(
|
106
|
+
required(:applicationExpirationDate).filled(:str?, :yymmdd?)
|
114
107
|
|
115
|
-
required(
|
108
|
+
required(:currencyCode).filled(:str?, :iso4217_numeric?)
|
116
109
|
|
117
|
-
|
110
|
+
required(:transactionAmount).filled(:int?)
|
118
111
|
|
119
|
-
|
112
|
+
optional(:cardholderName).filled(:str?)
|
120
113
|
|
121
|
-
required(
|
114
|
+
required(:deviceManufacturerIdentifier).filled(:str?, :hex?)
|
122
115
|
|
123
|
-
required(
|
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
|
-
|
128
|
-
|
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
|
132
|
-
|
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
|
-
|
137
|
-
validation = TokenSchema.call(token)
|
145
|
+
class Error < StandardError; end
|
138
146
|
|
139
|
-
|
147
|
+
module InstanceMethods
|
148
|
+
attr_reader :output
|
140
149
|
|
141
|
-
|
142
|
-
|
150
|
+
def validate
|
151
|
+
@validation ||= @schema.call(@input)
|
143
152
|
|
144
|
-
|
145
|
-
validate_token(token)
|
146
|
-
rescue TokenFormatError
|
147
|
-
false
|
148
|
-
end
|
153
|
+
@output = @validation.output
|
149
154
|
|
150
|
-
|
151
|
-
validation = TokenDataSchema.call(token_data)
|
155
|
+
return true if @validation.success?
|
152
156
|
|
153
|
-
|
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
|
-
|
169
|
+
@validation.errors
|
170
|
+
end
|
156
171
|
end
|
157
172
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
165
|
-
|
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
|
data/lib/pedicel/version.rb
CHANGED
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
|
+
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-
|
11
|
+
date: 2018-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-validation
|