Class: PedicelPay::Backend

Inherits:
Object
  • Object
show all
Defined in:
lib/pedicel-pay/backend.rb

Constant Summary collapse

Error =
Class.new(PedicelPay::Error)
CertificateError =
Class.new(PedicelPay::Backend::Error)
KeyError =
Class.new(PedicelPay::Backend::Error)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ca_key: nil, ca_certificate: nil, intermediate_key: nil, intermediate_certificate: nil, leaf_key: nil, leaf_certificate: nil) ⇒ Backend

Returns a new instance of Backend.



16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/pedicel-pay/backend.rb', line 16

def initialize(ca_key: nil,           ca_certificate: nil,
               intermediate_key: nil, intermediate_certificate: nil,
               leaf_key: nil,         leaf_certificate: nil)
  @ca_key         = ca_key
  @ca_certificate = ca_certificate

  @intermediate_key         = intermediate_key
  @intermediate_certificate = intermediate_certificate

  @leaf_key         = leaf_key
  @leaf_certificate = leaf_certificate
end

Instance Attribute Details

#ca_certificateObject

Returns the value of attribute ca_certificate.



11
12
13
# File 'lib/pedicel-pay/backend.rb', line 11

def ca_certificate
  @ca_certificate
end

#ca_keyObject

Returns the value of attribute ca_key.



11
12
13
# File 'lib/pedicel-pay/backend.rb', line 11

def ca_key
  @ca_key
end

#intermediate_certificateObject

Returns the value of attribute intermediate_certificate.



11
12
13
# File 'lib/pedicel-pay/backend.rb', line 11

def intermediate_certificate
  @intermediate_certificate
end

#intermediate_keyObject

Returns the value of attribute intermediate_key.



11
12
13
# File 'lib/pedicel-pay/backend.rb', line 11

def intermediate_key
  @intermediate_key
end

#leaf_certificateObject

Returns the value of attribute leaf_certificate.



11
12
13
# File 'lib/pedicel-pay/backend.rb', line 11

def leaf_certificate
  @leaf_certificate
end

#leaf_keyObject

Returns the value of attribute leaf_key.



11
12
13
# File 'lib/pedicel-pay/backend.rb', line 11

def leaf_key
  @leaf_key
end

Class Method Details

.generate(config: PedicelPay.config) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
# File 'lib/pedicel-pay/backend.rb', line 148

def self.generate(config: PedicelPay.config)
  ck, cc = generate_ca(config: config)

  ik, ic = generate_intermediate(ca_key: ck, ca_certificate: cc, config: config)

  lk, lc = generate_leaf(intermediate_key: ik, intermediate_certificate: ic, config: config)

  new(ca_key: ck, ca_certificate: cc,
      intermediate_key: ik, intermediate_certificate: ic,
      leaf_key: lk, leaf_certificate: lc)
end

.generate_ca(config: PedicelPay.config) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/pedicel-pay/backend.rb', line 160

def self.generate_ca(config: PedicelPay.config)
  key = OpenSSL::PKey::EC.new(PedicelPay::EC_CURVE)
  key.generate_key

  cert = OpenSSL::X509::Certificate.new
  cert.version = 2 # https://www.ietf.org/rfc/rfc5280.txt -> Section 4.1, search for "v3(2)".
  cert.serial = 1
  cert.subject = config[:subject][:ca]
  cert.issuer = cert.subject # Self-signed
  cert.public_key = PedicelPay::Helper.ec_key_to_pkey_public_key(key)
  cert.not_before = config[:valid].min
  cert.not_after = config[:valid].max

  ef = OpenSSL::X509::ExtensionFactory.new
  ef.subject_certificate = cert
  ef.issuer_certificate = cert
  cert.add_extension(ef.create_extension('basicConstraints','CA:TRUE',true))
  cert.add_extension(ef.create_extension('keyUsage','keyCertSign, cRLSign', true))
  cert.add_extension(ef.create_extension('subjectKeyIdentifier','hash',false))
  cert.add_extension(ef.create_extension('authorityKeyIdentifier','keyid:always',false))
  cert.sign(key, OpenSSL::Digest::SHA256.new)

  [key, cert]
end

.generate_intermediate(ca_key:, ca_certificate:, config: PedicelPay.config) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/pedicel-pay/backend.rb', line 185

def self.generate_intermediate(ca_key:, ca_certificate:, config: PedicelPay.config)
  key = OpenSSL::PKey::EC.new(PedicelPay::EC_CURVE)
  key.generate_key

  cert = OpenSSL::X509::Certificate.new
  # https://www.ietf.org/rfc/rfc5280.txt -> Section 4.1, search for "v3(2)".
  cert.version = 2
  cert.serial = 1
  cert.subject = config[:subject][:intermediate]
  cert.issuer = ca_certificate.subject
  cert.public_key = PedicelPay::Helper.ec_key_to_pkey_public_key(key)
  cert.not_before = config[:valid].min
  cert.not_after = config[:valid].max

  ef = OpenSSL::X509::ExtensionFactory.new
  ef.subject_certificate = cert
  ef.issuer_certificate = ca_certificate

  # According to https://tools.ietf.org/html/rfc5280#section-4.2.1.9,
  # CA:TRUE must be set in order to allow signing using this intermediate
  # certificate.
  cert.add_extension(ef.create_extension('basicConstraints', 'CA:TRUE', true))

  cert.add_extension(ef.create_extension('keyUsage', 'keyCertSign, cRLSign', true))
  cert.add_extension(ef.create_extension('subjectKeyIdentifier', 'hash', false))

  cert.add_extension(OpenSSL::X509::Extension.new(config[:oid][:intermediate_certificate], ''))

  cert.sign(ca_key, OpenSSL::Digest::SHA256.new)

  [key, cert]
end

.generate_leaf(intermediate_key:, intermediate_certificate:, config: PedicelPay.config) ⇒ Object



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/pedicel-pay/backend.rb', line 218

def self.generate_leaf(intermediate_key:, intermediate_certificate:, config: PedicelPay.config)
  key = OpenSSL::PKey::EC.new(PedicelPay::EC_CURVE)
  key.generate_key

  cert = OpenSSL::X509::Certificate.new
  cert.version = 2 # https://www.ietf.org/rfc/rfc5280.txt -> Section 4.1, search for "v3(2)".
  cert.serial = 1
  cert.subject = config[:subject][:leaf]
  cert.issuer = intermediate_certificate.subject
  cert.public_key = PedicelPay::Helper.ec_key_to_pkey_public_key(key)
  cert.not_before = config[:valid].min
  cert.not_after = config[:valid].max

  ef = OpenSSL::X509::ExtensionFactory.new
  ef.subject_certificate = cert
  ef.issuer_certificate = intermediate_certificate
  cert.add_extension(ef.create_extension('keyUsage','digitalSignature', true))
  cert.add_extension(ef.create_extension('subjectKeyIdentifier','hash',false))

  cert.add_extension(OpenSSL::X509::Extension.new(config[:oid][:leaf_certificate], ''))

  cert.sign(intermediate_key, OpenSSL::Digest::SHA256.new)

  [key, cert]
end

.generate_shared_secret_and_ephemeral_pubkey(recipient:) ⇒ Object



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/pedicel-pay/backend.rb', line 132

def self.generate_shared_secret_and_ephemeral_pubkey(recipient:)
  pubkey = case recipient
           when Client
             OpenSSL::PKey::EC.new(recipient.certificate.public_key).public_key
           when OpenSSL::X509::Certificate
             OpenSSL::PKey::EC.new(recipient.public_key).public_key
           when OpenSSL::PKey::EC::Point
             recipient
           else raise ArgumentError, 'invalid recipient'
           end

  ephemeral_seckey = OpenSSL::PKey::EC.new(PedicelPay::EC_CURVE).generate_key

  [ephemeral_seckey.dh_compute_key(pubkey), ephemeral_seckey.public_key]
end

Instance Method Details

#encrypt(token, recipient:, shared_secret: nil, ephemeral_pubkey: nil) ⇒ Object

Raises:

  • (ArgumentError)


64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/pedicel-pay/backend.rb', line 64

def encrypt(token, recipient:, shared_secret: nil, ephemeral_pubkey: nil)
  raise ArgumentError, 'invalid token' unless token.is_a?(Token)

  if shared_secret && ephemeral_pubkey
    # Use them. No check that they come from the same ephemeral secret key.
  elsif shared_secret.nil? ^ ephemeral_pubkey.nil?
    raise ArgumentError, "'shared_secret' and 'ephemeral_pubkey' must be supplied together"
  else # None of shared_secret or ephemeral_pubkey is supplied.
    shared_secret, ephemeral_pubkey = self.class.generate_shared_secret_and_ephemeral_pubkey(recipient: recipient)
  end

  symmetric_key = Pedicel::EC.symmetric_key(shared_secret: shared_secret, merchant_id: Helper.merchant_id(recipient))

  token.encrypted_data = Helper.encrypt(
    data: token.unencrypted_data.to_json,
    key: symmetric_key
  )

  token.header.ephemeral_pubkey = ephemeral_pubkey
  token.update_pubkey_hash(recipient: recipient)

  token
end

#encrypt_and_sign(token, recipient:, shared_secret: nil, ephemeral_pubkey: nil) ⇒ Object



57
58
59
60
61
62
# File 'lib/pedicel-pay/backend.rb', line 57

def encrypt_and_sign(token, recipient:, shared_secret: nil, ephemeral_pubkey: nil)
  encrypt(token, recipient: recipient, shared_secret: shared_secret, ephemeral_pubkey: ephemeral_pubkey)
  sign(token)

  token
end

#generate_client(valid: PedicelPay.config[:valid]) ⇒ Object



29
30
31
32
33
34
35
36
37
# File 'lib/pedicel-pay/backend.rb', line 29

def generate_client(valid: PedicelPay.config[:valid])
  client = PedicelPay::Client.new(ca_certificate_pem: ca_certificate.to_pem)
  client.generate_key

  client.ca_certificate_pem = ca_certificate.to_pem
  client.certificate = sign_csr(client.generate_csr, valid: valid)

  client
end

#sign(token, certificate: leaf_certificate, key: leaf_key, replace: true) ⇒ Object

Raises:

  • (ArgumentError)


88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/pedicel-pay/backend.rb', line 88

def sign(token, certificate: leaf_certificate, key: leaf_key, replace: true)
  raise ArgumentError, 'token has no encrypted_data' unless token.encrypted_data
  raise ArgumentError, 'token has no ephemeral_pubkey' unless token.header.ephemeral_pubkey

  message = [
    Helper.ec_key_to_pkey_public_key(token.header.ephemeral_pubkey).to_der,
    token.encrypted_data,
    token.header.transaction_id,
    token.header.data_hash
  ].compact.join

  signature = OpenSSL::PKCS7.sign(
    certificate,
    key,
    message,
    [intermediate_certificate, ca_certificate], # Chain.
    OpenSSL::PKCS7::BINARY # Handle 0x00 correctly.
  )

  # Check that the newly created signature is good.
  flags = \
    # https://wiki.openssl.org/index.php/Manual:PKCS7_verify(3)#VERIFY_PROCESS
    OpenSSL::PKCS7::NOCHAIN  | # Ignore certs in the message.
    OpenSSL::PKCS7::NOINTERN   # Only look at the supplied certificate.
  trust_store = OpenSSL::X509::Store.new
  trust_store.add_cert(ca_certificate).add_cert(intermediate_certificate)
  unless signature.verify([certificate], trust_store, message, flags)
    fail 'signature is wrong'
  end

  if replace
    # Just replace token.signature.
  else
    if token.signature # Already signed.
      existing_signature = OpenSSL::PKCS7.new(Base64.strict_decode64(token.signature))
      signature = existing_signature.add_signer(signature.signers.first)
    end
  end

  token.signature = Base64.strict_encode64(signature.to_der)

  token
end

#sign_csr(csr, valid: PedicelPay.config[:valid]) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/pedicel-pay/backend.rb', line 39

def sign_csr(csr, valid: PedicelPay.config[:valid])
  cert = OpenSSL::X509::Certificate.new
  cert.version = 2
  cert.serial = 1
  cert.not_before = valid.min
  cert.not_after = valid.max
  cert.subject = PedicelPay.config[:subject][:client]
  cert.public_key = csr.public_key
  cert.issuer = intermediate_certificate.issuer
  cert.sign(intermediate_key, OpenSSL::Digest::SHA256.new)

  merchant_id_hex = Helper.bytestring_to_hex(PedicelPay.config[:random].bytes(32))

  cert.add_extension(OpenSSL::X509::Extension.new(PedicelPay.config[:oid][:merchant_identifier_field], merchant_id_hex))

  cert
end

#validateObject



244
245
246
247
248
249
250
# File 'lib/pedicel-pay/backend.rb', line 244

def validate
  validate_ca
  validate_intermediate
  validate_leaf

  true
end

#validate_caObject

Raises:



252
253
254
255
256
257
258
# File 'lib/pedicel-pay/backend.rb', line 252

def validate_ca
  raise KeyError, 'ca private key not valid for ca certificate' unless ca_certificate.check_private_key(ca_key)

  raise CertificateError, 'ca certificate is not self-signed' unless ca_certificate.verify(ca_key)

  true
end

#validate_intermediateObject

Raises:



260
261
262
263
264
265
266
# File 'lib/pedicel-pay/backend.rb', line 260

def validate_intermediate
  raise KeyError, 'intermediate private key not valid for intermediate certificate' unless intermediate_certificate.check_private_key(intermediate_key)

  raise CertificateError, 'intermediate certificate not signed by ca' unless intermediate_certificate.verify(ca_key)

  true
end

#validate_leafObject

Raises:



268
269
270
271
272
273
274
# File 'lib/pedicel-pay/backend.rb', line 268

def validate_leaf
  raise KeyError, 'leaf private key not valid for leaf certificate' unless leaf_certificate.check_private_key(leaf_key)

  raise CertificateError, 'leaf certificate not signed by intermediate' unless leaf_certificate.verify(intermediate_key)

  true
end