pedicel 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 402a60cbe58a1a300764a3a37d53434c0e764f60
4
+ data.tar.gz: 1551470f672d864db8fec34b25eb0521c59de87b
5
+ SHA512:
6
+ metadata.gz: 3ec3218ec7911fbf7e2cd3d11b5fb3fc839be0f8e51694565f7cef655fdebc0d5133a906ea0bfc215e21da4030281f6c7f5c202fc3332e67b98a108db5d3c0ab
7
+ data.tar.gz: b770f9aff8ace1d06fda313fcdf9798bd84c9d78394b42539d22bbcd359db63956d201997477aa1e243b3c31fb936592db25cbd0d7cd29c79fdca47290258d91
data/lib/pedicel.rb ADDED
@@ -0,0 +1,46 @@
1
+ require 'pedicel/base'
2
+ require 'pedicel/ec'
3
+ require 'pedicel/rsa'
4
+
5
+ module Pedicel
6
+ class Error < StandardError; end
7
+ class TokenFormatError < Error; end
8
+ class SignatureError < Error; end
9
+ class VersionError < Error; end
10
+ class CertificateError < Error; end
11
+ class EcKeyError < Error; end
12
+
13
+ DEFAULTS = {
14
+ oids: {
15
+ intermediate_certificate: '1.2.840.113635.100.6.2.14',
16
+ leaf_certificate: '1.2.840.113635.100.6.29',
17
+ merchant_identifier_field: '1.2.840.113635.100.6.32',
18
+ },
19
+ replay_threshold_seconds: 3*60,
20
+ apple_root_ca_g3_cert_pem: <<~PEM
21
+ -----BEGIN CERTIFICATE-----
22
+ MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwS
23
+ QXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9u
24
+ IEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcN
25
+ MTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBS
26
+ b290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9y
27
+ aXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49
28
+ AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtf
29
+ TjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517
30
+ IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySr
31
+ MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gA
32
+ MGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4
33
+ at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM
34
+ 6BgD56KyKA==
35
+ -----END CERTIFICATE-----
36
+ PEM
37
+ }
38
+
39
+ def self.config
40
+ @@config ||= DEFAULTS
41
+ end
42
+
43
+ def self.config=(other)
44
+ @@config = other
45
+ end
46
+ end
@@ -0,0 +1,197 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Pedicel
5
+ class Base
6
+ SUPPORTED_VERSIONS = [:EC_v1]
7
+
8
+ def initialize(token, now: Time.now)
9
+ @token = token
10
+ end
11
+
12
+ def validate_content(now: Time.now)
13
+ raise VersionError, "unsupported version: #{version}" unless SUPPORTED_VERSIONS.include?(@token['version'])
14
+
15
+ raise ReplayAttackError, "token signature time indicates a replay attack (age #{now-cms_signing_time})" unless signing_time_ok?(now: now)
16
+
17
+ raise SignatureError unless valid_signature?
18
+ end
19
+
20
+ def version
21
+ @token['version']&.to_sym
22
+ end
23
+
24
+ def encrypted_data
25
+ return nil unless @token['data']
26
+
27
+ Base64.decode64(@token['data'])
28
+ end
29
+
30
+ def signature
31
+ return nil unless @token['signature']
32
+
33
+ Base64.decode64(@token['signature'])
34
+ end
35
+
36
+ def transaction_id
37
+ [@token['header']['transactionId']].pack('H*')
38
+ end
39
+
40
+ def application_data
41
+ return nil unless @token['applicationData']
42
+
43
+ [@token['applicationData']].pack('H*')
44
+ end
45
+
46
+ def signing_time_ok?(now: Time.now)
47
+ # "Inspect the CMS signing time of the signature, as defined by section 11.3
48
+ # of RFC 5652. If the time signature and the transaction time differ by more
49
+ # than a few minutes, it's possible that the token is a replay attack."
50
+ # https://developer.apple.com/library/content/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html
51
+
52
+ delta = Pedicel.config[:replay_threshold_seconds]
53
+
54
+ cms_signing_time.between?(now - delta, now + delta)
55
+ # Deliberately ignoring leap seconds.
56
+ end
57
+
58
+ def private_key_class
59
+ {EC_v1: OpenSSL::PKey::EC, RSA_v1: OpenSSL::PKey::RSA}[version]
60
+ end
61
+
62
+ def symmetric_algorithm
63
+ {EC_v1: 'aes-256-gcm', RSA_v1: 'aes-128-gcm'}[version]
64
+ end
65
+
66
+ def decrypt_aes(key:)
67
+ raise TokenFormatError, 'no encrypted data present' unless encrypted_data
68
+
69
+ if OpenSSL::Cipher.new('aes-256-gcm').respond_to?(:iv_len=)
70
+ # Either because you use Ruby >=2.4's native openssl lib, or if you have a
71
+ # "recent enough" version of the openssl gem available.
72
+
73
+ cipher = OpenSSL::Cipher.new(symmetric_algorithm)
74
+ cipher.decrypt
75
+
76
+ cipher.key = key
77
+ cipher.iv_len = 16 # Must be set before the IV because default is 12 and
78
+ # only IVs of length `iv_len` will be accepted.
79
+ cipher.iv = "\x00".b * cipher.iv_len
80
+
81
+ split_position = encrypted_data.length - cipher.iv_len
82
+ tag = encrypted_data.slice(split_position, cipher.iv_len)
83
+ untagged_encrypted_data = encrypted_data.slice(0, split_position)
84
+
85
+ cipher.auth_tag = tag
86
+ cipher.auth_data = ''.b
87
+
88
+ cipher.update(untagged_encrypted_data) << cipher.final
89
+ else
90
+ require 'aes256gcm_decrypt'
91
+
92
+ Aes256GcmDecrypt::decrypt(encrypted_data, key)
93
+ end
94
+ end
95
+
96
+ def valid_signature?(now: Time.now)
97
+ true if validate_signature(now: now)
98
+ rescue
99
+ false
100
+ end
101
+
102
+ def verify_signature(ca_certificate_pem: Pedicel.config[:apple_root_ca_g3_cert_pem], now: Time.now)
103
+ raise SignatureError, 'no signature present' unless signature
104
+
105
+ begin
106
+ s = OpenSSL::PKCS7.new(signature)
107
+ rescue => e
108
+ raise SignatureError, "invalid PKCS #7 signature: #{e.message}"
109
+ end
110
+
111
+ # 1.a
112
+ # Ensure that the certificates contain the correct custom OIDs: (...).
113
+ # The value for these marker OIDs doesn't matter, only their presence.
114
+ leaf, intermediate = self.class.verify_signature_certificate_oids(signature: s)
115
+
116
+ begin
117
+ root = OpenSSL::X509::Certificate.new(ca_certificate_pem)
118
+ rescue => e
119
+ raise CertificateError, "invalid root certificate: #{e.message}"
120
+ end
121
+
122
+ # 1.b
123
+ # Ensure that the root CA is the Apple Root CA - G3. (...)
124
+ self.class.verify_root_certificate(root: root, intermediate: intermediate)
125
+
126
+ # 1.c
127
+ # Ensure that there is a valid X.509 chain of trust from the signature to the root CA.
128
+ self.class.verify_x509_chain(root: root, intermediate: intermediate, leaf: leaf)
129
+
130
+ # 1.d
131
+ # Validate the token's signature.
132
+ #
133
+ # Implemented in the subclass.
134
+ validate_signature(signature: s, leaf: leaf)
135
+
136
+ # 1.e
137
+ # Inspect the CMS signing time of the signature (...)
138
+ self.class.verify_signed_time(signature: s, now: now)
139
+
140
+ self
141
+ end
142
+
143
+ private
144
+
145
+ def self.verify_signature_certificate_oids(signature:)
146
+ leaf = signature.certificates.find{|c| c.extensions.find{|e| e.oid == Pedicel.config[:oids][:leaf_certificate]}}
147
+ unless leaf
148
+ raise SignatureError, "no leaf certificate found (OID #{Pedicel.config[:oids][:leaf_certificate]})"
149
+ end
150
+
151
+ intermediate = signature.certificates.find{|c| c.extensions.find{|e| e.oid == Pedicel.config[:oids][:intermediate_certificate]}}
152
+ unless intermediate
153
+ raise SignatureError, "no intermediate certificate found (OID #{Pedicel.config[:oids][:leaf_certificate]})"
154
+ end
155
+
156
+ [leaf, intermediate]
157
+ end
158
+
159
+ def self.verify_root_certificate(root:, intermediate:)
160
+ unless intermediate.issuer == root.subject
161
+ raise SignatureError, 'root certificate has not issued intermediate certificate'
162
+ end
163
+ end
164
+
165
+ def self.verify_x509_chain(root:, intermediate:, leaf:)
166
+ valid_chain = OpenSSL::X509::Store.new.
167
+ add_cert(root).
168
+ add_cert(intermediate).
169
+ verify(leaf)
170
+
171
+ raise SignatureError, 'invalid chain of trust' unless valid_chain
172
+ end
173
+
174
+ def self.verify_signed_time(signature:, now:)
175
+ # Inspect the CMS signing time of the signature, as defined by section
176
+ # 11.3 of RFC 5652. If the time signature and the transaction time differ
177
+ # by more than a few minutes, it's possible that the token is a replay
178
+ # attack.
179
+
180
+ unless signature.signers.length == 1
181
+ raise SignatureError, 'not 1 signer, unable to determine signing time'
182
+ end
183
+ signed_time = signature.signers.first.signed_time
184
+
185
+ few_min = Pedicel.config[:replay_threshold_seconds]
186
+
187
+ # Time objects. DST aware. Ignoring leap seconds.
188
+ return if signed_time.between?(now - few_min, now + few_min) # Both ends included.
189
+
190
+ diff = signed_time - now
191
+ if diff.negative?
192
+ raise SignatureError, "signature too old; signed #{-diff.to_i}s ago"
193
+ end
194
+ raise SignatureError, "signature too new; signed #{diff.to_i}s in the future"
195
+ end
196
+ end
197
+ end
data/lib/pedicel/ec.rb ADDED
@@ -0,0 +1,168 @@
1
+ require 'pedicel/base'
2
+
3
+ module Pedicel
4
+ class EC < Base
5
+ def ephemeral_public_key
6
+ Base64.decode64(@token['header']['ephemeralPublicKey'])
7
+ end
8
+
9
+ def decrypt(symmetric_key: nil, merchant_id: nil, certificate: nil, private_key: nil,
10
+ ca_certificate_pem: Pedicel.config[:apple_root_ca_g3_cert_pem], now: Time.now)
11
+ raise ArgumentError 'invalid argument combination' unless \
12
+ !symmetric_key.nil? ^ ((!merchant_id.nil? ^ !certificate.nil?) && !private_key.nil?)
13
+ # .-------------------'--------. .----------'----. .-------------''---.
14
+ # | symmetric_key can be | | merchant_id | | Both private_key |
15
+ # | derived from private_key | | (byte string) | | and merchant_id |
16
+ # | and the shared_secret--- | | can be | | is necessary to |
17
+ # | which can be derived from | | derived from | | derive the |
18
+ # | the private_key and the | | the public | | symmetric key |
19
+ # | token's ephemeralPublicKey | | certificate | '------------------'
20
+ # '----------------------------' '---------------'
21
+
22
+ if private_key
23
+ symmetric_key = symmetric_key(private_key: private_key,
24
+ certificate: certificate,
25
+ merchant_id: merchant_id)
26
+ end
27
+
28
+ verify_signature(ca_certificate_pem: ca_certificate_pem, now: now)
29
+ decrypt_aes(key: symmetric_key)
30
+ end
31
+
32
+ def symmetric_key(shared_secret: nil, private_key: nil, merchant_id: nil, certificate: nil)
33
+ raise ArgumentError 'invalid argument combination' unless \
34
+ (!shared_secret.nil? ^ !private_key.nil?) && (!merchant_id.nil? ^ !certificate.nil?)
35
+ # .--------------------'. .----------------'| .-----------------'--.
36
+ # | shared_secret can | | shared_secret | | merchant_id (byte |
37
+ # | be derived from the | | and merchant_id | | string can be |
38
+ # | private_key and the | | is necessary to | | derived from the |
39
+ # | ephemeralPublicKey | | derive the | | public certificate |
40
+ # '---------------------' | symmetric_key | '--------------------'
41
+ # '-----------------'
42
+
43
+ shared_secret = shared_secret(private_key: private_key) if private_key
44
+ merchant_id = self.class.merchant_id(certificate: certificate) if certificate
45
+
46
+ self.class.symmetric_key(shared_secret: shared_secret, merchant_id: merchant_id)
47
+ end
48
+
49
+ # Extract the shared secret from one public key (the ephemeral) and one
50
+ # private key.
51
+ def shared_secret(private_key:)
52
+ begin
53
+ privkey = OpenSSL::PKey::EC.new(private_key)
54
+ rescue => e
55
+ raise EcKeyError, "invalid PEM format of private key for EC: #{e.message}"
56
+ end
57
+
58
+ begin
59
+ pubkey = OpenSSL::PKey::EC.new(ephemeral_public_key).public_key
60
+ rescue => e
61
+ raise EcKeyError, "invalid format of ephemeralPublicKey (from token) for EC: #{e.message}"
62
+ end
63
+
64
+ unless privkey.group == pubkey.group
65
+ raise EcKeyError,
66
+ "private_key curve '#{privkey.group.curve_name}' differ from " \
67
+ "ephemeralPublicKey (from token) curve '#{pubkey.group.curve_name}'"
68
+ end
69
+
70
+ privkey.dh_compute_key(pubkey)
71
+ end
72
+
73
+ def self.symmetric_key(merchant_id:, shared_secret:)
74
+ # http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar2.pdf
75
+ # Section 5.8.1.1, The Single-Step KDF Specification.
76
+ #
77
+ # With slight adjustments:
78
+ # > 1. Set `reps = ceil(keydatalen/hashlen)`
79
+ # > 2. If `reps > (2^32 - 1)`, then return an error indicator without
80
+ # > performing the remaining actions.
81
+ # > 3. Initialize a 32-bit, big-endian bit string `counter` as 00000001 base
82
+ # > 16 (i.e. 0x00000001).
83
+ # > 4. If `counter || Z || OtherInfo` is more than `max_H_inputlen` bits
84
+ # > long, then return an error indicator without performing the remaining
85
+ # > actions.
86
+ # > 5. For `i = 1` to `reps` by `1`, do the following:
87
+ # > 5.1 Compute `K(i) = H(counter || Z || OtherInfo)`.
88
+ # > 5.2 Increment `counter` (modulo `2^32`), treating it as an
89
+ # > unsigned 32-bit integer.
90
+ # > 6. Let `K_Last` be set to `K(reps)` if `keydatalen / hashlen` is an
91
+ # > integer; otherwise, let `K_Last` be set to the `keydatalen mod
92
+ # > hashlen` leftmost bits of `K(reps)`.
93
+ # > 7. Return `K(1) || K(2) || ... || K(reps-1) || K_Last`.
94
+ #
95
+ # Digest::SHA256 will do the calculations when we throw Z and OtherInfo into
96
+ # the digest.
97
+
98
+ sha256 = Digest::SHA256.new
99
+
100
+ # Step 3:
101
+ sha256 << "\x00\x00\x00\x01"
102
+
103
+ # Z:
104
+ sha256 << shared_secret
105
+
106
+ # OtherInfo:
107
+ # https://developer.apple.com/library/content/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html
108
+ sha256 << "\x0d" + 'id-aes256-GCM' # AlgorithmID
109
+ sha256 << 'Apple' # PartyUInfo
110
+ sha256 << merchant_id # PartyVInfo
111
+
112
+ sha256.digest
113
+ end
114
+
115
+ def self.merchant_id(certificate:)
116
+ begin
117
+ cert = OpenSSL::X509::Certificate.new(certificate)
118
+ rescue => e
119
+ raise CertificateError, "invalid PEM format of certificate: #{e.message}"
120
+ end
121
+
122
+ merchant_id_hex =
123
+ cert.
124
+ extensions.
125
+ find { |x| x.oid == Pedicel.config[:oids][:merchant_identifier_field] }&.
126
+ value&. # Hex encoded Merchant ID plus perhaps extra non-hex chars.
127
+ delete("^[0-9a-fA-F]") # Remove non-hex chars.
128
+
129
+ raise CertificateError, 'no merchant identifier in certificate' unless merchant_id_hex
130
+
131
+ [merchant_id_hex].pack('H*')
132
+ end
133
+
134
+ private
135
+
136
+ def validate_signature(signature:, leaf:)
137
+ # (...) ensure that the signature is a valid ECDSA signature
138
+ # (ecdsa-with-SHA256 1.2.840.10045.4.3.2) of the concatenated values of
139
+ # the ephemeralPublicKey, data, transactionId, and applicationData keys.
140
+
141
+ unless leaf.signature_algorithm == 'ecdsa-with-SHA256'
142
+ raise SignatureError, 'signature algorithm is not ecdsa-with-SHA256'
143
+ end
144
+
145
+ message = [
146
+ ephemeral_public_key,
147
+ encrypted_data,
148
+ transaction_id,
149
+ application_data,
150
+ ].compact.join
151
+
152
+ # https://wiki.openssl.org/index.php/Manual:PKCS7_verify(3)#VERIFY_PROCESS
153
+ flags = \
154
+ OpenSSL::PKCS7::NOCHAIN | # Ignore certs in the message.
155
+ OpenSSL::PKCS7::NOINTERN | # Only look at the supplied certificate.
156
+ OpenSSL::PKCS7::NOVERIFY # Do not verify the chain; already done.
157
+
158
+ # Trust exactly the leaf which has already been verified.
159
+ certificates = [leaf]
160
+
161
+ store = OpenSSL::X509::Store.new
162
+
163
+ unless signature.verify(certificates, store, message, flags)
164
+ raise SignatureError, 'signature does not match the message'
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,15 @@
1
+ require 'pedicel/base'
2
+
3
+ module Pedicel
4
+ class RSA < Base
5
+
6
+ private
7
+
8
+ def symmetric_key(private_key)
9
+ # RSA/ECB/OAEPWithSHA256AndMGF1Padding
10
+
11
+ # OpenSSL::PKey::RSA#private_decrypt will use SHA1. Only. :-(
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,138 @@
1
+ require 'dry-validation'
2
+ require 'base64'
3
+
4
+ module Pedicel
5
+ class Validator
6
+ class Error < StandardError; end
7
+ class TokenFormatError < Error; end
8
+ class TokenDataFormatError < Error; end
9
+
10
+ DRY_CUSTOM_PREDICATE_ERRORS = {
11
+ base64?: 'invalid base64',
12
+ hex?: 'invalid hex',
13
+ pan?: 'invalid pan',
14
+ yymmdd?: 'invalid date format YYMMDD',
15
+ }
16
+
17
+ module Predicates
18
+ include Dry::Logic::Predicates
19
+
20
+ predicate(:base64?) { |value| !Base64.decode64(value).nil? rescue false }
21
+
22
+ predicate(:hex?) { |value| !Regexp.new(/\A[a-f0-9-]*\z/i).match(value).nil? }
23
+
24
+ predicate(:pan?) { |value| !Regexp.new(/\A[0-9]{13,19}\z/).match(value).nil? }
25
+
26
+ predicate(:yymmdd?) do |value|
27
+ return false unless value.length == 6
28
+ Time.new(value[0..1],value[2..3], value[4..5]).is_a?(Time) rescue false
29
+ end
30
+ end
31
+
32
+ TokenSchema = Dry::Validation.Schema do
33
+ configure do
34
+ predicates(Predicates)
35
+ def self.messages
36
+ super.merge(en: { errors: DRY_CUSTOM_PREDICATE_ERRORS })
37
+ end
38
+ end
39
+
40
+ required(:data).filled(:str?, :base64?)
41
+
42
+ required(:header).schema do
43
+ optional(:applicationData).filled(:str?, :hex?)
44
+
45
+ optional(:ephemeralPublicKey).filled(:str?, :base64?)
46
+ optional(:wrappedKey).filled(:str?, :base64?)
47
+ rule('ephemeralPublicKey xor wrappedKey': [:ephemeralPublicKey, :wrappedKey]) do |e, w|
48
+ e.filled? ^ w.filled?
49
+ end
50
+
51
+ required(:publicKeyHash).filled(:str?, :base64?)
52
+
53
+ required(:transactionId).filled(:str?, :hex?)
54
+ end
55
+
56
+ required(:signature).filled(:str?, :base64?)
57
+
58
+ required(:version).filled(:str?, included_in?: ['EC_v1', 'RSA_v1'])
59
+ end
60
+
61
+ # Pedicel::Validator::TokenSchema.call({data: 'asdf', header: {ephemeralPublicKey: 'e', publicKeyHash: 'p', transactionId: 'f'}, signature: 's', version: 'EC_v1'})
62
+
63
+ TokenDataSchema = Dry::Validation.Schema do
64
+ configure do
65
+ predicates(Predicates)
66
+ def self.messages
67
+ super.merge(en: { errors: DRY_CUSTOM_PREDICATE_ERRORS })
68
+ end
69
+ end
70
+
71
+ required(:applicationPrimaryAccountNumber).filled(:str?, :pan?)
72
+
73
+ required(:applicationExpirationDate).filled(:str?, :yymmdd?)
74
+
75
+ required(:currencyCode).filled(:str?, format?: /\A[0-9]{3}\z/)
76
+
77
+ required(:transactionAmount).filled(:int?)
78
+
79
+ optional(:cardholderName).filled(:str?)
80
+
81
+ required(:deviceManufacturerIdentifier).filled(:str?, :hex?)
82
+
83
+ required(:paymentDataType).filled(:str?, included_in?: ['3DSecure', 'EMV'])
84
+
85
+ required(:paymentData).schema do
86
+ optional(:onlinePaymentCryptogram).filled(:str?, :base64?)
87
+ optional(:eciIndicator).filled(:str?)
88
+
89
+ optional(:emvData).filled(:str?, :base64?)
90
+ optional(:encryptedPINData).filled(:str?, :hex?)
91
+ end
92
+
93
+ rule('consistent paymentDataType and paymentData': [:paymentDataType, [:paymentData, :onlinePaymentCryptogram]]) do |t, cryptogram|
94
+ t.eql?('3DSecure') > cryptogram.filled?
95
+ end
96
+ end
97
+
98
+ # Pedicel::Validator::TokenDataSchema.call(
99
+ # applicationPrimaryAccountNumber: '1234567890123',
100
+ # applicationExpirationDate: '101112',
101
+ # currencyCode: '123',
102
+ # transactionAmount: 12.34,
103
+ # cardholderName: 'asdf',
104
+ # deviceManufacturerIdentifier: 'adsf',
105
+ # paymentDataType: 'asdf',
106
+ # )
107
+
108
+ def self.validate_token(token, now: Time.now)
109
+ validation = TokenSchema.call(token)
110
+
111
+ raise TokenFormatError, validation.hints.map{|key,msg| "#{key} #{msg}"}.join(', and ') unless validation.errors.empty?
112
+
113
+ true
114
+ end
115
+
116
+ def self.valid_token?(token, now: Time.now)
117
+ validate_token(token, now: now) rescue false
118
+ end
119
+
120
+ def self.validate_token_data(token_data)
121
+ validation = TokenDataSchema.call(token_data)
122
+
123
+ raise TokenDataFormatError, validation.hints.map{|key,msg| "#{key} #{msg}"}.join(', and ') unless validation.errors.empty?
124
+
125
+ true
126
+ end
127
+
128
+ def self.valid_token_data?(token_data)
129
+ validate_token_data(token_data) rescue false
130
+ end
131
+
132
+ def validate_content(now: Time.now)
133
+ raise ReplayAttackError, "token signature time indicates a replay attack (age #{now-cms_signing_time})" unless signing_time_ok?(now: now)
134
+
135
+ raise SignatureError unless valid_signature?
136
+ end
137
+ end
138
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pedicel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Clearhaus
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-02-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-validation
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.11.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.11.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: aes256gcm_decrypt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.0.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.11'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.11'
69
+ description:
70
+ email: hello@clearhaus.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - lib/pedicel.rb
76
+ - lib/pedicel/base.rb
77
+ - lib/pedicel/ec.rb
78
+ - lib/pedicel/rsa.rb
79
+ - lib/pedicel/validator.rb
80
+ homepage: https://github.com/clearhaus/pedicel-pay
81
+ licenses:
82
+ - MIT
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project:
100
+ rubygems_version: 2.5.2
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Backend and client part of Apple Pay
104
+ test_files: []