pedicel-pay 0.0.1 → 0.0.2
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/exe/pedicel-pay +182 -0
- data/lib/pedicel-pay/backend.rb +261 -0
- data/lib/pedicel-pay/client.rb +42 -0
- data/lib/pedicel-pay/helper.rb +56 -0
- data/lib/pedicel-pay/token.rb +74 -0
- data/lib/pedicel-pay/token_data.rb +152 -0
- data/lib/pedicel-pay/token_header.rb +36 -0
- data/lib/pedicel-pay/version.rb +3 -0
- data/lib/pedicel-pay.rb +43 -0
- metadata +20 -29
- data/Dockerfile +0 -9
- data/Gemfile +0 -3
- data/LICENSE.txt +0 -21
- data/README.md +0 -99
- data/pedicel-pay.gemspec +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 68626ff88a9448225853d1c8547535236f4a734fe850618a3cf5ee7593c4ccee
|
4
|
+
data.tar.gz: 64cbedfe02190675426b05e0db34f386be38c38c46a4d00029bd22578630f687
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 070601a57849d8d89aff29301cf9b299beba45f6f0ba6d2922f4be7c969c4d69ad0cf0e108bc0946116f078fad7fb349dd712ed063da856bc854d2da8f3f54ef
|
7
|
+
data.tar.gz: 92b6937df63bd603bd8abf06a261b57af47aa9898a4a8cfbf179079f8e74f4bf4a9b658028a58561098b07704b75e0223ef7668f83424ade10d245bcac24bf86
|
data/exe/pedicel-pay
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'thor'
|
5
|
+
require 'pedicel-pay'
|
6
|
+
require 'json'
|
7
|
+
require 'time'
|
8
|
+
|
9
|
+
module PedicelPay
|
10
|
+
class Cli < Thor
|
11
|
+
BACKEND_FILES = [
|
12
|
+
'ca-key.pem',
|
13
|
+
'ca-certificate.pem',
|
14
|
+
'intermediate-key.pem',
|
15
|
+
'intermediate-certificate.pem',
|
16
|
+
'leaf-key.pem',
|
17
|
+
'leaf-certificate.pem',
|
18
|
+
]
|
19
|
+
|
20
|
+
CLIENT_FILES = [
|
21
|
+
'client-key.pem',
|
22
|
+
'client-certificate.pem',
|
23
|
+
]
|
24
|
+
|
25
|
+
FILES = BACKEND_FILES | CLIENT_FILES
|
26
|
+
|
27
|
+
|
28
|
+
desc 'clean', 'Remove all generated files'
|
29
|
+
option :path, type: :string, path: true, aliases: :p
|
30
|
+
|
31
|
+
def clean
|
32
|
+
FILES.
|
33
|
+
map { |f| options[:path] ? File.join(File.expand_path(options[:path]), f) : f }.
|
34
|
+
each { |f| File.delete(f) if File.exist?(f) }
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
desc 'generate-backend', 'Generate a backend: CA, intermediate and leaf certificates and keys'
|
39
|
+
option 'force', type: :boolean, aliases: :f
|
40
|
+
option 'destination', type: :string, aliases: [:dest, :d]
|
41
|
+
option 'path', type: :string, path: true, aliases: :p
|
42
|
+
option 'valid-from', type: :string
|
43
|
+
option 'valid-to', type: :string
|
44
|
+
|
45
|
+
def generate_backend
|
46
|
+
path = File.expand_path(options[:path] || '.')
|
47
|
+
|
48
|
+
BACKEND_FILES.map{|f| File.join(path, f)}.each do |file|
|
49
|
+
if File.exist?(file)
|
50
|
+
$stderr.puts "File #{file} already exist"
|
51
|
+
exit 1
|
52
|
+
end
|
53
|
+
end unless options[:force]
|
54
|
+
|
55
|
+
Dir.mkdir(path) unless File.directory?(path)
|
56
|
+
|
57
|
+
PedicelPay.config[:valid] = \
|
58
|
+
(options['valid-from'] ? Time.parse(options['valid-from']) : PedicelPay.config[:valid].min) ..
|
59
|
+
(options['valid-to'] ? Time.parse(options['valid-to']) : PedicelPay.config[:valid].max)
|
60
|
+
|
61
|
+
backend = PedicelPay::Backend.generate
|
62
|
+
|
63
|
+
File.open(File.join(path, 'ca-key.pem'), 'w') { |f| f.write(backend.ca_key.to_pem) }
|
64
|
+
File.open(File.join(path, 'ca-certificate.pem'), 'w') { |f| f.write(backend.ca_certificate.to_pem) }
|
65
|
+
File.open(File.join(path, 'intermediate-key.pem'), 'w') { |f| f.write(backend.intermediate_key.to_pem) }
|
66
|
+
File.open(File.join(path, 'intermediate-certificate.pem'), 'w') { |f| f.write(backend.intermediate_certificate.to_pem) }
|
67
|
+
File.open(File.join(path, 'leaf-key.pem'), 'w') { |f| f.write(backend.leaf_key.to_pem) }
|
68
|
+
File.open(File.join(path, 'leaf-certificate.pem'), 'w') { |f| f.write(backend.leaf_certificate.to_pem) }
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
desc 'check-backend', 'Check that the backend created is functional'
|
73
|
+
option :path, type: :string, path: true, aliases: :p
|
74
|
+
|
75
|
+
def check_backend
|
76
|
+
backend = Helper.load_backend(options[:path])
|
77
|
+
|
78
|
+
backend.is_a?(PedicelPay::Backend) && backend.validate
|
79
|
+
rescue => e
|
80
|
+
$stderr.puts e
|
81
|
+
exit 1
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
desc 'generate-client', 'Generate a client, the merchant side'
|
86
|
+
option 'backend-path', type: :string, path: true, aliases: :b
|
87
|
+
option 'force', type: :boolean, aliases: :f
|
88
|
+
option 'path', type: :string, path: true, aliases: :p
|
89
|
+
option 'valid-from', type: :string
|
90
|
+
option 'valid-to', type: :string
|
91
|
+
|
92
|
+
def generate_client
|
93
|
+
path = File.expand_path(options[:path] || '.')
|
94
|
+
|
95
|
+
CLIENT_FILES.map{|f| File.join(path, f)}.each do |file|
|
96
|
+
if File.exist?(file)
|
97
|
+
$stderr.puts "File #{file} already exist"
|
98
|
+
exit 1
|
99
|
+
end
|
100
|
+
end unless options[:force]
|
101
|
+
|
102
|
+
Dir.mkdir(path) unless File.directory?(path)
|
103
|
+
|
104
|
+
valid = \
|
105
|
+
(options['valid-from'] ? Time.parse(options['valid-from']) : PedicelPay.config[:valid].min) ..
|
106
|
+
(options['valid-to'] ? Time.parse(options['valid-to']) : PedicelPay.config[:valid].max)
|
107
|
+
|
108
|
+
client = Helper.load_backend(options['backend-path']).generate_client(valid: valid)
|
109
|
+
|
110
|
+
File.open(File.join(path, 'client-key.pem'), 'w') { |f| f.write(client.key.to_pem) }
|
111
|
+
File.open(File.join(path, 'client-certificate.pem'), 'w') { |f| f.write(client.certificate.to_pem) }
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
desc 'generate-token', 'Generate a token'
|
116
|
+
option 'backend-path', type: :string, path: true, aliases: :b
|
117
|
+
option 'client-path', type: :string, path: true, aliases: :c
|
118
|
+
option 'amount', type: :string
|
119
|
+
option 'currency', type: :string
|
120
|
+
option 'pan', type: :string
|
121
|
+
option 'expiry', type: :string
|
122
|
+
|
123
|
+
def generate_token
|
124
|
+
backend = Helper.load_backend(options['backend-path'])
|
125
|
+
client = Helper.load_client(options['client-path'])
|
126
|
+
|
127
|
+
token = PedicelPay::Token.new
|
128
|
+
token.sample
|
129
|
+
|
130
|
+
token.unencrypted_data.amount = options['amount'].to_i if options['amount']&.to_i
|
131
|
+
token.unencrypted_data.currency = options['currency'] if options['currency']
|
132
|
+
token.unencrypted_data.pan = options['pan'] if options['pan']
|
133
|
+
token.unencrypted_data.expiry = options['expiry'] if options['expiry']
|
134
|
+
|
135
|
+
backend.encrypt_and_sign(token, recipient: client)
|
136
|
+
|
137
|
+
puts token.to_json
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
desc 'decrypt-token', 'Decrypt a token'
|
142
|
+
option 'client-path', type: :string, path: true, aliases: :c
|
143
|
+
option 'backend-path', type: :string, path: true, aliases: :b
|
144
|
+
option 'file', type: :string, aliases: :f
|
145
|
+
|
146
|
+
def decrypt_token
|
147
|
+
raw_token = options['file'] ? File.read(options['file']) : $stdin.read
|
148
|
+
token = JSON.parse(raw_token)
|
149
|
+
|
150
|
+
client = Helper.load_client(options['client-path'])
|
151
|
+
backend = Helper.load_backend(options['backend-path'])
|
152
|
+
|
153
|
+
puts client.decrypt(token, ca_certificate_pem: backend.ca_certificate.to_pem)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class Helper
|
158
|
+
def self.load_backend(path)
|
159
|
+
path = File.expand_path(path || '.')
|
160
|
+
|
161
|
+
PedicelPay::Backend.new(
|
162
|
+
ca_key: OpenSSL::PKey::EC.new( File.read(File.join(path, 'ca-key.pem'))),
|
163
|
+
ca_certificate: OpenSSL::X509::Certificate.new(File.read(File.join(path, 'ca-certificate.pem'))),
|
164
|
+
intermediate_key: OpenSSL::PKey::EC.new( File.read(File.join(path, 'intermediate-key.pem'))),
|
165
|
+
intermediate_certificate: OpenSSL::X509::Certificate.new(File.read(File.join(path, 'intermediate-certificate.pem'))),
|
166
|
+
leaf_key: OpenSSL::PKey::EC.new( File.read(File.join(path, 'leaf-key.pem'))),
|
167
|
+
leaf_certificate: OpenSSL::X509::Certificate.new(File.read(File.join(path, 'leaf-certificate.pem')))
|
168
|
+
)
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.load_client(path)
|
172
|
+
path = File.expand_path(path || '.')
|
173
|
+
|
174
|
+
PedicelPay::Client.new(
|
175
|
+
key: OpenSSL::PKey::EC.new( File.read(File.join(path, 'client-key.pem'))),
|
176
|
+
certificate: OpenSSL::X509::Certificate.new(File.read(File.join(path, 'client-certificate.pem'))),
|
177
|
+
)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
PedicelPay::Cli.start(ARGV)
|
@@ -0,0 +1,261 @@
|
|
1
|
+
require 'pedicel-pay/helper'
|
2
|
+
require 'pedicel'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module PedicelPay
|
6
|
+
class Backend
|
7
|
+
Error = Class.new(PedicelPay::Error)
|
8
|
+
CertificateError = Class.new(PedicelPay::Backend::Error)
|
9
|
+
KeyError = Class.new(PedicelPay::Backend::Error)
|
10
|
+
|
11
|
+
attr_accessor \
|
12
|
+
:ca_key, :ca_certificate,
|
13
|
+
:intermediate_key, :intermediate_certificate,
|
14
|
+
:leaf_key, :leaf_certificate
|
15
|
+
|
16
|
+
def initialize(ca_key: nil, ca_certificate: nil,
|
17
|
+
intermediate_key: nil, intermediate_certificate: nil,
|
18
|
+
leaf_key: nil, leaf_certificate: nil)
|
19
|
+
@ca_key = ca_key
|
20
|
+
@ca_certificate = ca_certificate
|
21
|
+
|
22
|
+
@intermediate_key = intermediate_key
|
23
|
+
@intermediate_certificate = intermediate_certificate
|
24
|
+
|
25
|
+
@leaf_key = leaf_key
|
26
|
+
@leaf_certificate = leaf_certificate
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_client(valid: PedicelPay.config[:valid])
|
30
|
+
client = PedicelPay::Client.new(ca_certificate_pem: ca_certificate.to_pem)
|
31
|
+
client.generate_key
|
32
|
+
|
33
|
+
client.ca_certificate_pem = ca_certificate.to_pem
|
34
|
+
client.certificate = sign_csr(client.generate_csr, valid: valid)
|
35
|
+
|
36
|
+
client
|
37
|
+
end
|
38
|
+
|
39
|
+
def sign_csr(csr, valid: PedicelPay.config[:valid])
|
40
|
+
cert = OpenSSL::X509::Certificate.new
|
41
|
+
cert.version = 2
|
42
|
+
cert.serial = 1
|
43
|
+
cert.not_before = valid.min
|
44
|
+
cert.not_after = valid.max
|
45
|
+
cert.subject = PedicelPay.config[:subject][:client]
|
46
|
+
cert.public_key = csr.public_key
|
47
|
+
cert.issuer = intermediate_certificate.issuer
|
48
|
+
cert.sign(intermediate_key, OpenSSL::Digest::SHA256.new)
|
49
|
+
|
50
|
+
merchant_id_hex = Helper.bytestring_to_hex(PedicelPay.config[:random].bytes(32))
|
51
|
+
|
52
|
+
cert.add_extension(OpenSSL::X509::Extension.new(PedicelPay.config[:oid][:merchant_identifier_field], merchant_id_hex))
|
53
|
+
|
54
|
+
cert
|
55
|
+
end
|
56
|
+
|
57
|
+
def encrypt_and_sign(token, recipient:, shared_secret: nil, ephemeral_pubkey: nil)
|
58
|
+
encrypt(token, recipient: recipient, shared_secret: shared_secret, ephemeral_pubkey: ephemeral_pubkey)
|
59
|
+
sign(token)
|
60
|
+
|
61
|
+
token
|
62
|
+
end
|
63
|
+
|
64
|
+
def encrypt(token, recipient:, shared_secret: nil, ephemeral_pubkey: nil)
|
65
|
+
raise ArgumentError, 'invalid token' unless token.is_a?(Token)
|
66
|
+
|
67
|
+
if shared_secret && ephemeral_pubkey
|
68
|
+
# Use them. No check that they come from the same ephemeral secret key.
|
69
|
+
elsif shared_secret.nil? ^ ephemeral_pubkey.nil?
|
70
|
+
raise ArgumentError, "'shared_secret' and 'ephemeral_pubkey' must be supplied together"
|
71
|
+
else # None of shared_secret or ephemeral_pubkey is supplied.
|
72
|
+
shared_secret, ephemeral_pubkey = self.class.generate_shared_secret_and_ephemeral_pubkey(recipient: recipient)
|
73
|
+
end
|
74
|
+
|
75
|
+
symmetric_key = Pedicel::EC.symmetric_key(shared_secret: shared_secret, merchant_id: Helper.merchant_id(recipient))
|
76
|
+
|
77
|
+
token.encrypted_data = Helper.encrypt(
|
78
|
+
data: token.unencrypted_data.to_json,
|
79
|
+
key: symmetric_key
|
80
|
+
)
|
81
|
+
|
82
|
+
token.header.ephemeral_pubkey = ephemeral_pubkey
|
83
|
+
token.update_pubkey_hash(recipient: recipient)
|
84
|
+
|
85
|
+
token
|
86
|
+
end
|
87
|
+
|
88
|
+
def sign(token, certificate: leaf_certificate, key: leaf_key)
|
89
|
+
raise ArgumentError, 'token has no encrypted_data' unless token.encrypted_data
|
90
|
+
raise ArgumentError, 'token has no ephemeral_pubkey' unless token.header.ephemeral_pubkey
|
91
|
+
|
92
|
+
message = [
|
93
|
+
Helper.ec_key_to_pkey_public_key(token.header.ephemeral_pubkey).to_der,
|
94
|
+
token.encrypted_data,
|
95
|
+
token.header.transaction_id,
|
96
|
+
token.header.data_hash
|
97
|
+
].compact.join
|
98
|
+
|
99
|
+
signature = OpenSSL::PKCS7.sign(
|
100
|
+
certificate,
|
101
|
+
key,
|
102
|
+
message,
|
103
|
+
[intermediate_certificate, ca_certificate], # Chain.
|
104
|
+
OpenSSL::PKCS7::BINARY # Handle 0x00 correctly.
|
105
|
+
)
|
106
|
+
|
107
|
+
if token.signature # Already signed.
|
108
|
+
oldsig = OpenSSL::PKCS7.new(Base64.strict_decode64(token.signature))
|
109
|
+
signature = oldsig.add_signer(signature.signers.first)
|
110
|
+
end
|
111
|
+
|
112
|
+
token.signature = Base64.strict_encode64(signature.to_der)
|
113
|
+
|
114
|
+
token
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.generate_shared_secret_and_ephemeral_pubkey(recipient:)
|
118
|
+
pubkey = case recipient
|
119
|
+
when Client
|
120
|
+
OpenSSL::PKey::EC.new(recipient.certificate.public_key).public_key
|
121
|
+
when OpenSSL::X509::Certificate
|
122
|
+
OpenSSL::PKey::EC.new(recipient.public_key).public_key
|
123
|
+
when OpenSSL::PKey::EC::Point
|
124
|
+
recipient
|
125
|
+
else raise ArgumentError, 'invalid recipient'
|
126
|
+
end
|
127
|
+
|
128
|
+
ephemeral_seckey = OpenSSL::PKey::EC.new(PedicelPay::EC_CURVE).generate_key
|
129
|
+
|
130
|
+
[ephemeral_seckey.dh_compute_key(pubkey), ephemeral_seckey.public_key]
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.generate(config: PedicelPay.config)
|
134
|
+
ck, cc = generate_ca(config: config)
|
135
|
+
|
136
|
+
ik, ic = generate_intermediate(ca_key: ck, ca_certificate: cc, config: config)
|
137
|
+
|
138
|
+
lk, lc = generate_leaf(intermediate_key: ik, intermediate_certificate: ic, config: config)
|
139
|
+
|
140
|
+
new(ca_key: ck, ca_certificate: cc,
|
141
|
+
intermediate_key: ik, intermediate_certificate: ic,
|
142
|
+
leaf_key: lk, leaf_certificate: lc)
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.generate_ca(config: PedicelPay.config)
|
146
|
+
key = OpenSSL::PKey::EC.new(PedicelPay::EC_CURVE)
|
147
|
+
key.generate_key
|
148
|
+
|
149
|
+
cert = OpenSSL::X509::Certificate.new
|
150
|
+
cert.version = 2 # https://www.ietf.org/rfc/rfc5280.txt -> Section 4.1, search for "v3(2)".
|
151
|
+
cert.serial = 1
|
152
|
+
cert.subject = config[:subject][:ca]
|
153
|
+
cert.issuer = cert.subject # Self-signed
|
154
|
+
cert.public_key = PedicelPay::Helper.ec_key_to_pkey_public_key(key)
|
155
|
+
cert.not_before = config[:valid].min
|
156
|
+
cert.not_after = config[:valid].max
|
157
|
+
|
158
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
159
|
+
ef.subject_certificate = cert
|
160
|
+
ef.issuer_certificate = cert
|
161
|
+
cert.add_extension(ef.create_extension('basicConstraints','CA:TRUE',true))
|
162
|
+
cert.add_extension(ef.create_extension('keyUsage','keyCertSign, cRLSign', true))
|
163
|
+
cert.add_extension(ef.create_extension('subjectKeyIdentifier','hash',false))
|
164
|
+
cert.add_extension(ef.create_extension('authorityKeyIdentifier','keyid:always',false))
|
165
|
+
cert.sign(key, OpenSSL::Digest::SHA256.new)
|
166
|
+
|
167
|
+
[key, cert]
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.generate_intermediate(ca_key:, ca_certificate:, config: PedicelPay.config)
|
171
|
+
key = OpenSSL::PKey::EC.new(PedicelPay::EC_CURVE)
|
172
|
+
key.generate_key
|
173
|
+
|
174
|
+
cert = OpenSSL::X509::Certificate.new
|
175
|
+
# https://www.ietf.org/rfc/rfc5280.txt -> Section 4.1, search for "v3(2)".
|
176
|
+
cert.version = 2
|
177
|
+
cert.serial = 1
|
178
|
+
cert.subject = config[:subject][:intermediate]
|
179
|
+
cert.issuer = ca_certificate.subject
|
180
|
+
cert.public_key = PedicelPay::Helper.ec_key_to_pkey_public_key(key)
|
181
|
+
cert.not_before = config[:valid].min
|
182
|
+
cert.not_after = config[:valid].max
|
183
|
+
|
184
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
185
|
+
ef.subject_certificate = cert
|
186
|
+
ef.issuer_certificate = ca_certificate
|
187
|
+
|
188
|
+
# According to https://tools.ietf.org/html/rfc5280#section-4.2.1.9,
|
189
|
+
# CA:TRUE must be set in order to allow signing using this intermediate
|
190
|
+
# certificate.
|
191
|
+
cert.add_extension(ef.create_extension('basicConstraints', 'CA:TRUE', true))
|
192
|
+
|
193
|
+
cert.add_extension(ef.create_extension('keyUsage', 'keyCertSign, cRLSign', true))
|
194
|
+
cert.add_extension(ef.create_extension('subjectKeyIdentifier', 'hash', false))
|
195
|
+
|
196
|
+
cert.add_extension(OpenSSL::X509::Extension.new(config[:oid][:intermediate_certificate], ''))
|
197
|
+
|
198
|
+
cert.sign(ca_key, OpenSSL::Digest::SHA256.new)
|
199
|
+
|
200
|
+
[key, cert]
|
201
|
+
end
|
202
|
+
|
203
|
+
def self.generate_leaf(intermediate_key:, intermediate_certificate:, config: PedicelPay.config)
|
204
|
+
key = OpenSSL::PKey::EC.new(PedicelPay::EC_CURVE)
|
205
|
+
key.generate_key
|
206
|
+
|
207
|
+
cert = OpenSSL::X509::Certificate.new
|
208
|
+
cert.version = 2 # https://www.ietf.org/rfc/rfc5280.txt -> Section 4.1, search for "v3(2)".
|
209
|
+
cert.serial = 1
|
210
|
+
cert.subject = config[:subject][:leaf]
|
211
|
+
cert.issuer = intermediate_certificate.subject
|
212
|
+
cert.public_key = PedicelPay::Helper.ec_key_to_pkey_public_key(key)
|
213
|
+
cert.not_before = config[:valid].min
|
214
|
+
cert.not_after = config[:valid].max
|
215
|
+
|
216
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
217
|
+
ef.subject_certificate = cert
|
218
|
+
ef.issuer_certificate = intermediate_certificate
|
219
|
+
cert.add_extension(ef.create_extension('keyUsage','digitalSignature', true))
|
220
|
+
cert.add_extension(ef.create_extension('subjectKeyIdentifier','hash',false))
|
221
|
+
|
222
|
+
cert.add_extension(OpenSSL::X509::Extension.new(config[:oid][:leaf_certificate], ''))
|
223
|
+
|
224
|
+
cert.sign(intermediate_key, OpenSSL::Digest::SHA256.new)
|
225
|
+
|
226
|
+
[key, cert]
|
227
|
+
end
|
228
|
+
|
229
|
+
def validate
|
230
|
+
validate_ca
|
231
|
+
validate_intermediate
|
232
|
+
validate_leaf
|
233
|
+
|
234
|
+
true
|
235
|
+
end
|
236
|
+
|
237
|
+
def validate_ca
|
238
|
+
raise KeyError, 'ca private key not valid for ca certificate' unless ca_certificate.check_private_key(ca_key)
|
239
|
+
|
240
|
+
raise CertificateError, 'ca certificate is not self-signed' unless ca_certificate.verify(ca_key)
|
241
|
+
|
242
|
+
true
|
243
|
+
end
|
244
|
+
|
245
|
+
def validate_intermediate
|
246
|
+
raise KeyError, 'intermediate private key not valid for intermediate certificate' unless intermediate_certificate.check_private_key(intermediate_key)
|
247
|
+
|
248
|
+
raise CertificateError, 'intermediate certificate not signed by ca' unless intermediate_certificate.verify(ca_key)
|
249
|
+
|
250
|
+
true
|
251
|
+
end
|
252
|
+
|
253
|
+
def validate_leaf
|
254
|
+
raise KeyError, 'leaf private key not valid for leaf certificate' unless leaf_certificate.check_private_key(leaf_key)
|
255
|
+
|
256
|
+
raise CertificateError, 'leaf certificate not signed by intermediate' unless leaf_certificate.verify(intermediate_key)
|
257
|
+
|
258
|
+
true
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'pedicel-pay/helper'
|
2
|
+
require 'openssl'
|
3
|
+
require 'pedicel'
|
4
|
+
|
5
|
+
module PedicelPay
|
6
|
+
class Client
|
7
|
+
attr_accessor :key, :certificate, :ca_certificate_pem
|
8
|
+
|
9
|
+
def initialize(key: nil, certificate: nil, ca_certificate_pem: nil)
|
10
|
+
@key = key
|
11
|
+
@certificate = certificate
|
12
|
+
@ca_certificate_pem = ca_certificate_pem
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate_key
|
16
|
+
@key = OpenSSL::PKey::EC.new(PedicelPay::EC_CURVE)
|
17
|
+
@key.generate_key
|
18
|
+
|
19
|
+
@key
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate_csr(subject: PedicelPay.config[:subject][:csr])
|
23
|
+
req = OpenSSL::X509::Request.new
|
24
|
+
req.version = 0
|
25
|
+
req.subject = subject
|
26
|
+
req.public_key = PedicelPay::Helper.ec_key_to_pkey_public_key(key)
|
27
|
+
req.sign(key, OpenSSL::Digest::SHA256.new)
|
28
|
+
|
29
|
+
req
|
30
|
+
end
|
31
|
+
|
32
|
+
def merchant_id
|
33
|
+
Pedicel::EC.merchant_id(certificate: certificate)
|
34
|
+
end
|
35
|
+
|
36
|
+
def decrypt(token, ca_certificate_pem: @ca_certificate_pem, now: Time.now)
|
37
|
+
Pedicel::EC.
|
38
|
+
new(token).
|
39
|
+
decrypt(private_key: key, certificate: certificate, ca_certificate_pem: ca_certificate_pem, now: now)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PedicelPay
|
4
|
+
# Assistance/collection functions.
|
5
|
+
class Helper
|
6
|
+
def initialize(config: PedicelPay.config, pedicel_instance: nil)
|
7
|
+
@config = config
|
8
|
+
@pedicel = pedicel_instance
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.ec_key_to_pkey_public_key(ec_key)
|
12
|
+
# EC#public_key is not a PKey public key, but an EC point.
|
13
|
+
pub = OpenSSL::PKey::EC.new(ec_key.group)
|
14
|
+
pub.public_key = ec_key.is_a?(OpenSSL::PKey::PKey) ? ec_key.public_key : ec_key
|
15
|
+
|
16
|
+
pub
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.bytestring_to_hex(string)
|
20
|
+
string.unpack('H*').first
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.merchant_id(x)
|
24
|
+
case x
|
25
|
+
when Client
|
26
|
+
Pedicel::EC.merchant_id(certificate: x.certificate)
|
27
|
+
when OpenSSL::X509::Certificate
|
28
|
+
Pedicel::EC.merchant_id(certificate: x)
|
29
|
+
when /\A[0-9a-fA-F]{64}\z/
|
30
|
+
[x].pack('H*')
|
31
|
+
when /\A.{32}\z/
|
32
|
+
x
|
33
|
+
else
|
34
|
+
raise ArgumentError, "cannot extract 'merchant_id' from #{x}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.recipient_certificate(recipient:)
|
39
|
+
case recipient
|
40
|
+
when Client then recipient.certificate
|
41
|
+
when OpenSSL::X509::Certificate then recipient
|
42
|
+
else raise ArgumentError, 'invalid recipient'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.encrypt(data:, key:)
|
47
|
+
cipher = OpenSSL::Cipher.new('aes-256-gcm')
|
48
|
+
cipher.encrypt
|
49
|
+
cipher.key = key
|
50
|
+
cipher.iv_len = 16
|
51
|
+
cipher.iv = 0.chr * cipher.iv_len
|
52
|
+
cipher.auth_data = ''
|
53
|
+
cipher.update(data) + cipher.final + cipher.auth_tag
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'pedicel-pay/token_data'
|
2
|
+
require 'pedicel-pay/token_header'
|
3
|
+
require 'digest'
|
4
|
+
|
5
|
+
module PedicelPay
|
6
|
+
class Token
|
7
|
+
Error = Class.new(PedicelPay::Error)
|
8
|
+
|
9
|
+
attr_accessor \
|
10
|
+
:header,
|
11
|
+
:signature,
|
12
|
+
:unencrypted_data,
|
13
|
+
:encrypted_data,
|
14
|
+
:version
|
15
|
+
|
16
|
+
def initialize(unencrypted_data: nil, encrypted_data: nil, header: nil, signature: nil, version: 'EC_v1')
|
17
|
+
@unencrypted_data = unencrypted_data
|
18
|
+
@encrypted_data = encrypted_data
|
19
|
+
@header = header
|
20
|
+
@signature = signature
|
21
|
+
@version = version
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_pubkey_hash(recipient:)
|
25
|
+
pubkey = Helper.recipient_certificate(recipient: recipient)
|
26
|
+
|
27
|
+
header.pubkey_hash = Digest::SHA256.base64digest(pubkey.to_der)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_json
|
31
|
+
to_hash.to_json
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_hash
|
35
|
+
raise Error, 'no encrypted data' unless encrypted_data
|
36
|
+
|
37
|
+
{
|
38
|
+
'data' => Base64.strict_encode64(encrypted_data),
|
39
|
+
'header' => header.to_hash,
|
40
|
+
'signature' => signature,
|
41
|
+
'version' => version,
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def sample
|
46
|
+
sample_data
|
47
|
+
sample_header
|
48
|
+
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def sample_data
|
53
|
+
return if encrypted_data
|
54
|
+
|
55
|
+
if unencrypted_data
|
56
|
+
unencrypted_data.sample
|
57
|
+
else
|
58
|
+
self.unencrypted_data = TokenData.new.sample
|
59
|
+
end
|
60
|
+
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def sample_header
|
65
|
+
if header
|
66
|
+
header.sample
|
67
|
+
else
|
68
|
+
self.header = TokenHeader.new.sample
|
69
|
+
end
|
70
|
+
|
71
|
+
self
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module PedicelPay
|
4
|
+
class TokenData
|
5
|
+
Error = Class.new(PedicelPay::Error)
|
6
|
+
DateError = Class.new(Error)
|
7
|
+
|
8
|
+
attr_accessor \
|
9
|
+
:pan,
|
10
|
+
:expiry,
|
11
|
+
:currency,
|
12
|
+
:amount,
|
13
|
+
:name,
|
14
|
+
:dm_id,
|
15
|
+
:cryptogram,
|
16
|
+
:eci
|
17
|
+
|
18
|
+
CURRENCIES = %w[
|
19
|
+
008 012 032 036 044 048 050 051 052 060 064 068 072 084 090 096 104 108
|
20
|
+
116 124 132 136 144 152 156 170 174 188 191 192 203 208 214 222 230 232
|
21
|
+
238 242 262 270 292 320 324 328 332 340 344 348 352 356 360 364 368 376
|
22
|
+
388 392 398 400 404 408 410 414 417 418 422 426 430 434 446 454 458 462
|
23
|
+
480 484 496 498 504 512 516 524 532 533 548 554 558 566 578 586 590 598
|
24
|
+
600 604 608 634 643 646 654 682 690 694 702 704 706 710 728 748 752 756
|
25
|
+
760 764 776 780 784 788 800 807 818 826 834 840 858 860 882 886 901 929
|
26
|
+
930 931 932 933 934 936 937 938 940 941 943 944 946 947 948 949 950 951
|
27
|
+
952 953 955 956 957 958 959 960 961 962 963 964 965 967 968 969 970 971
|
28
|
+
972 973 975 976 977 978 979 980 981 984 985 986 990 994 997 999
|
29
|
+
].freeze
|
30
|
+
|
31
|
+
def initialize(pan: nil, expiry: nil, currency: nil, amount: nil,
|
32
|
+
name: nil, dm_id: nil, cryptogram: nil, eci: nil)
|
33
|
+
@pan = pan
|
34
|
+
@expiry = expiry
|
35
|
+
@currency = currency
|
36
|
+
@amount = amount
|
37
|
+
@name = name
|
38
|
+
@dm_id = dm_id
|
39
|
+
@cryptogram = cryptogram
|
40
|
+
@eci = eci
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_hash
|
44
|
+
data = { onlinePaymentCryptogram: cryptogram }
|
45
|
+
data[:eciIndicator] = eci if eci
|
46
|
+
|
47
|
+
result = {
|
48
|
+
applicationPrimaryAccountNumber: pan,
|
49
|
+
applicationExpirationDate: expiry,
|
50
|
+
currencyCode: currency,
|
51
|
+
transactionAmount: amount,
|
52
|
+
deviceManufacturerIdentifier: dm_id,
|
53
|
+
paymentDataType: '3DSecure',
|
54
|
+
paymentData: data
|
55
|
+
}
|
56
|
+
|
57
|
+
result[:cardholderName] = name if name
|
58
|
+
|
59
|
+
result
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_json
|
63
|
+
to_hash.to_json
|
64
|
+
end
|
65
|
+
|
66
|
+
def sample(expired: nil, pan_length: nil)
|
67
|
+
# PAN
|
68
|
+
# Override @pan if pan_length doesn't match.
|
69
|
+
if pan.nil? || (pan_length && pan.length != pan_length)
|
70
|
+
pan_length ||= [12, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 19, 19, 19].sample
|
71
|
+
|
72
|
+
self.pan = [ [2, 4, 5, 6].sample, *(2..pan_length).map { rand(0..9) } ].join
|
73
|
+
end
|
74
|
+
|
75
|
+
# Expiry
|
76
|
+
# Override @expiry if it doesn't match `expired`.
|
77
|
+
# WARNING: Time calculations ahead!
|
78
|
+
# Think very carefully about all the crazy corner cases.
|
79
|
+
now = Time.now
|
80
|
+
if expiry.nil? || (expired ^ card_expired?(now)) # Cannot use "soon".
|
81
|
+
self.expiry = self.class.sample_expiry(expired: expired, now: now, soon: now + 5 * 60)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Currency
|
85
|
+
self.currency ||= CURRENCIES.sample
|
86
|
+
|
87
|
+
# Amount
|
88
|
+
self.amount ||= rand(100..99_999)
|
89
|
+
|
90
|
+
# Name
|
91
|
+
|
92
|
+
# Device Manufacturer Identification
|
93
|
+
self.dm_id ||= Helper.bytestring_to_hex(PedicelPay.config[:random].bytes(5))
|
94
|
+
|
95
|
+
# Cryptogram
|
96
|
+
self.cryptogram ||= Base64.strict_encode64(PedicelPay.config[:random].bytes(10))
|
97
|
+
|
98
|
+
# ECI
|
99
|
+
self.eci ||= %w[05 06 07].sample
|
100
|
+
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
def card_expired?(now)
|
105
|
+
Time.parse(expired) <= now
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.sample_expiry(expired: nil, now: nil, soon: nil)
|
109
|
+
# WARNING: Time calculations ahead!
|
110
|
+
# Think very carefully about all the crazy corner cases.
|
111
|
+
|
112
|
+
now ||= Time.now
|
113
|
+
soon ||= now + 5 * 60
|
114
|
+
|
115
|
+
year = sample_expiry_year(expired: expired, soon: soon)
|
116
|
+
month = sample_expiry_month(expired: expired, year: year, now: now, soon: soon)
|
117
|
+
|
118
|
+
require 'date'
|
119
|
+
Date.civil(year, month, -1).strftime('%y%m%d')
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.sample_expiry_year(expired: nil, soon:)
|
123
|
+
# WARNING: Time calculations ahead!
|
124
|
+
# Think very carefully about all the crazy corner cases.
|
125
|
+
|
126
|
+
case expired
|
127
|
+
when nil then -5..6
|
128
|
+
when true then -5..0
|
129
|
+
when false then 0..6
|
130
|
+
end
|
131
|
+
.map { |i| soon.year + i }
|
132
|
+
.to_a.sample
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.sample_expiry_month(expired: nil, year:, now:, soon:)
|
136
|
+
# WARNING: Time calculations ahead!
|
137
|
+
# Think very carefully about all the crazy corner cases.
|
138
|
+
|
139
|
+
case expired
|
140
|
+
when nil
|
141
|
+
1..12
|
142
|
+
when true
|
143
|
+
year < now.year ? 1..12 : 1..(now.month - 1)
|
144
|
+
when false
|
145
|
+
raise DateError, 'cannot expire in a soon future year' if expired && year > soon.year
|
146
|
+
|
147
|
+
year == soon.year ? 1..soon.month : 1..12
|
148
|
+
end
|
149
|
+
.to_a.sample
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module PedicelPay
|
2
|
+
class TokenHeader
|
3
|
+
Error = Class.new(PedicelPay::Error)
|
4
|
+
|
5
|
+
attr_accessor \
|
6
|
+
:data_hash,
|
7
|
+
:ephemeral_pubkey,
|
8
|
+
:pubkey_hash,
|
9
|
+
:transaction_id
|
10
|
+
|
11
|
+
def initialize(data_hash: nil, ephemeral_pubkey: nil, pubkey_hash: nil, transaction_id: nil)
|
12
|
+
@data_hash, @ephemeral_pubkey, @pubkey_hash, @transaction_id = \
|
13
|
+
data_hash, ephemeral_pubkey, pubkey_hash, transaction_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_hash
|
17
|
+
raise Error, 'missing ephemeral_pubkey' unless ephemeral_pubkey
|
18
|
+
|
19
|
+
result = {
|
20
|
+
'ephemeralPublicKey' => Base64.strict_encode64(Helper.ec_key_to_pkey_public_key(ephemeral_pubkey).to_der),
|
21
|
+
'publicKeyHash' => pubkey_hash,
|
22
|
+
'transactionId' => Helper.bytestring_to_hex(transaction_id),
|
23
|
+
}
|
24
|
+
result.merge!('applicationData' => data_hash) if data_hash
|
25
|
+
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
def sample
|
30
|
+
self.transaction_id ||= PedicelPay.config[:random].bytes(32)
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
data/lib/pedicel-pay.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module PedicelPay
|
4
|
+
class Error < StandardError; end
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'pedicel-pay/backend'
|
8
|
+
require 'pedicel-pay/client'
|
9
|
+
require 'pedicel-pay/token'
|
10
|
+
|
11
|
+
module PedicelPay
|
12
|
+
EC_CURVE = 'prime256v1'
|
13
|
+
|
14
|
+
DEFAULTS = {
|
15
|
+
oid: {
|
16
|
+
intermediate_certificate: '1.2.840.113635.100.6.2.14',
|
17
|
+
leaf_certificate: '1.2.840.113635.100.6.29',
|
18
|
+
merchant_identifier_field: '1.2.840.113635.100.6.32',
|
19
|
+
},
|
20
|
+
subject: {
|
21
|
+
ca: OpenSSL::X509::Name.parse('/C=DK/O=Pedicel Inc./OU=Pedicel Certification Authority/CN=Pedicel Root CA - G3'),
|
22
|
+
intermediate: OpenSSL::X509::Name.parse('/C=DK/O=Pedicel Inc./OU=Pedicel Certification Authority/CN=Pedicel Application Integration CA - G3'),
|
23
|
+
leaf: OpenSSL::X509::Name.parse('/C=DK/O=Pedicel Inc./OU=pOS Systems/CN=ecc-smp-broker-sign_UC4-PROD'),
|
24
|
+
csr: OpenSSL::X509::Name.parse('/CN=merchant-url.tld'),
|
25
|
+
client: OpenSSL::X509::Name.parse('/UID=merchant-url.tld.pedicel-merchant.PedicelMerchant/CN=Merchant ID: merchant-url.tld.pedicel-merchant.PedicelMerchant/OU=1W2X3Y4Z5A/O=PedicelMerchant Inc./C=DK'),
|
26
|
+
},
|
27
|
+
random: Random.new,
|
28
|
+
valid: Time.new(Time.now.year - 1)..Time.new(Time.now.year + 2),
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
def self.config
|
32
|
+
@@config ||= DEFAULTS.dup
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Monkey-patch to make OpenSSL::X509::Certificate#sign work.
|
37
|
+
if OpenSSL::PKey::EC.new.respond_to?(:private_key?) && !OpenSSL::PKey::EC.new.respond_to?(:private?)
|
38
|
+
class OpenSSL::PKey::EC
|
39
|
+
def private?
|
40
|
+
private_key?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pedicel-pay
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Clearhaus A/S
|
@@ -24,20 +24,6 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.16'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: pry
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
27
|
- !ruby/object:Gem::Dependency
|
42
28
|
name: pedicel
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -56,41 +42,46 @@ dependencies:
|
|
56
42
|
name: thor
|
57
43
|
requirement: !ruby/object:Gem::Requirement
|
58
44
|
requirements:
|
59
|
-
- - "
|
45
|
+
- - "~>"
|
60
46
|
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
47
|
+
version: '0.20'
|
62
48
|
type: :runtime
|
63
49
|
prerelease: false
|
64
50
|
version_requirements: !ruby/object:Gem::Requirement
|
65
51
|
requirements:
|
66
|
-
- - "
|
52
|
+
- - "~>"
|
67
53
|
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
54
|
+
version: '0.20'
|
69
55
|
- !ruby/object:Gem::Dependency
|
70
56
|
name: openssl
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
72
58
|
requirements:
|
73
|
-
- - "
|
59
|
+
- - "~>"
|
74
60
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
61
|
+
version: '2.1'
|
76
62
|
type: :runtime
|
77
63
|
prerelease: false
|
78
64
|
version_requirements: !ruby/object:Gem::Requirement
|
79
65
|
requirements:
|
80
|
-
- - "
|
66
|
+
- - "~>"
|
81
67
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
68
|
+
version: '2.1'
|
83
69
|
description:
|
84
70
|
email: hello@clearhaus.com
|
85
|
-
executables:
|
71
|
+
executables:
|
72
|
+
- pedicel-pay
|
86
73
|
extensions: []
|
87
74
|
extra_rdoc_files: []
|
88
75
|
files:
|
89
|
-
-
|
90
|
-
-
|
91
|
-
-
|
92
|
-
-
|
93
|
-
- pedicel-pay.
|
76
|
+
- exe/pedicel-pay
|
77
|
+
- lib/pedicel-pay.rb
|
78
|
+
- lib/pedicel-pay/backend.rb
|
79
|
+
- lib/pedicel-pay/client.rb
|
80
|
+
- lib/pedicel-pay/helper.rb
|
81
|
+
- lib/pedicel-pay/token.rb
|
82
|
+
- lib/pedicel-pay/token_data.rb
|
83
|
+
- lib/pedicel-pay/token_header.rb
|
84
|
+
- lib/pedicel-pay/version.rb
|
94
85
|
homepage: https://github.com/clearhaus/pedicel-pay
|
95
86
|
licenses:
|
96
87
|
- MIT
|
data/Dockerfile
DELETED
data/Gemfile
DELETED
data/LICENSE.txt
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
The MIT License (MIT)
|
2
|
-
|
3
|
-
Copyright (c) 2018 Clearhaus A/S
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
7
|
-
in the Software without restriction, including without limitation the rights
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
10
|
-
furnished to do so, subject to the following conditions:
|
11
|
-
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
13
|
-
all copies or substantial portions of the Software.
|
14
|
-
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
-
THE SOFTWARE.
|
data/README.md
DELETED
@@ -1,99 +0,0 @@
|
|
1
|
-
# PedicelPay
|
2
|
-
|
3
|
-
A tool to handle the server and client side of Apple Pay. It consist of both a
|
4
|
-
CLI part and a Ruby library e.g. for testing purposes.
|
5
|
-
|
6
|
-
## Usage (CLI)
|
7
|
-
|
8
|
-
### Setup a backend and a client
|
9
|
-
|
10
|
-
1. Generate a backend (Apple side of Apple Pay):
|
11
|
-
|
12
|
-
$ pedicel-pay generate-backend
|
13
|
-
|
14
|
-
Creates these files:
|
15
|
-
* `ca.key`
|
16
|
-
* `ca-certificate.pem`
|
17
|
-
* `intermediate.key`
|
18
|
-
* `intermediate-certificate.pem`
|
19
|
-
* `leaf.key`
|
20
|
-
* `leaf-certificate.pem`
|
21
|
-
|
22
|
-
2. Generate a client (merchant side of Apple Pay):
|
23
|
-
|
24
|
-
$ pedicel-pay generate-client
|
25
|
-
|
26
|
-
Creates `client.key` and `client-certificate.pem`.
|
27
|
-
|
28
|
-
|
29
|
-
### Create tokens
|
30
|
-
|
31
|
-
$ pedicel-pay generate-token \
|
32
|
-
--pan=4111111111111111 \
|
33
|
-
--expiry=$(date -d 'next year' +%y%m%d) \
|
34
|
-
--amount=1234 \
|
35
|
-
--currency=978
|
36
|
-
|
37
|
-
Specify some values, sample remaining:
|
38
|
-
|
39
|
-
$ pedicel-pay generate-token \
|
40
|
-
--pan=4111111111111111 \
|
41
|
-
--sample
|
42
|
-
|
43
|
-
### Decrypt tokens
|
44
|
-
|
45
|
-
$ echo $TOKEN | pedicel-pay decrypt-token
|
46
|
-
|
47
|
-
|
48
|
-
## Usage (Ruby)
|
49
|
-
|
50
|
-
### Setup a backend and a client
|
51
|
-
|
52
|
-
```ruby
|
53
|
-
backend = PedicelPay::Backend.generate
|
54
|
-
client = backend.generate_client
|
55
|
-
```
|
56
|
-
|
57
|
-
### Create tokens
|
58
|
-
|
59
|
-
Sample data
|
60
|
-
|
61
|
-
```ruby
|
62
|
-
token = PedicelPay::Token.new.sample
|
63
|
-
backend.encrypt(token: token, recipient: client)
|
64
|
-
backend.sign(token)
|
65
|
-
puts token.to_json
|
66
|
-
```
|
67
|
-
|
68
|
-
or decide:
|
69
|
-
|
70
|
-
```ruby
|
71
|
-
token = PedicelPay::Token.new
|
72
|
-
|
73
|
-
token.unencrypted_data.pan = '4111111111111111'
|
74
|
-
token.unencrypted_data.currency = '987' # EUR
|
75
|
-
token.unencrypted_data.amount = 1234 # 12.34 EUR
|
76
|
-
token.sample # Sample remaining.
|
77
|
-
|
78
|
-
backend.encrypt(token: token, recipient: client)
|
79
|
-
backend.sign(token)
|
80
|
-
puts token.to_json
|
81
|
-
```
|
82
|
-
|
83
|
-
The JSON formatted Payment Token; refer to
|
84
|
-
https://developer.apple.com/library/content/documentation/PassKit/Reference/PaymentTokenJSON/PaymentTokenJSON.html
|
85
|
-
|
86
|
-
|
87
|
-
### Decrypt tokens
|
88
|
-
|
89
|
-
Using the `client` (if it knows the CA cert):
|
90
|
-
|
91
|
-
```ruby
|
92
|
-
client.decrypt(JSON.parse(token.to_json))
|
93
|
-
```
|
94
|
-
|
95
|
-
To decrypt the token data by hand, use these values:
|
96
|
-
* The client's secret key `client.key`.
|
97
|
-
* The merchant ID `client.merchant_id` or client's certificate (containing the
|
98
|
-
merchant ID) `client.certificate`.
|
99
|
-
* Use `backend.ca_certificate` as Apple Root CA G3 certificate.
|
data/pedicel-pay.gemspec
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
lib = File.expand_path('../lib', __FILE__)
|
2
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
-
|
4
|
-
require 'pedicel-pay/version'
|
5
|
-
|
6
|
-
Gem::Specification.new do |s|
|
7
|
-
s.name = 'pedicel-pay'
|
8
|
-
s.version = PedicelPay::VERSION
|
9
|
-
s.author = 'Clearhaus A/S'
|
10
|
-
s.email = 'hello@clearhaus.com'
|
11
|
-
|
12
|
-
s.summary = 'Backend and client part of Apple Pay'
|
13
|
-
s.homepage = 'https://github.com/clearhaus/pedicel-pay'
|
14
|
-
s.license = 'MIT'
|
15
|
-
|
16
|
-
s.files = `ls`.split.reject {|f| f.match(/^spec\//) }
|
17
|
-
s.bindir = 'exe'
|
18
|
-
s.executables = s.files.grep(/^exe\//) { |f| File.basename(f) }
|
19
|
-
s.require_paths = ['lib']
|
20
|
-
|
21
|
-
s.add_development_dependency 'bundler', '~> 1.16'
|
22
|
-
s.add_development_dependency 'pry'
|
23
|
-
s.add_runtime_dependency 'pedicel', '~> 0.0.2'
|
24
|
-
s.add_runtime_dependency 'thor'
|
25
|
-
s.add_runtime_dependency 'openssl' # for ruby <= 2.3
|
26
|
-
end
|