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 +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
|