Coverage for jbank/wspki.py : 0%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import base64
2import logging
3import traceback
4from typing import Optional
5import requests
6from django.utils.timezone import now
7from django.utils.translation import ugettext as _
8from jbank.csr_helpers import (
9 create_private_key,
10 get_private_key_pem,
11 strip_pem_header_and_footer,
12 create_csr_pem,
13 write_private_key_pem_file,
14 load_private_key_from_pem_file,
15)
16from jbank.models import WsEdiConnection, WsEdiSoapCall, PayoutParty
17from lxml import etree # type: ignore # pytype: disable=import-error
18from jbank.x509_helpers import get_x509_cert_from_file, write_cert_pem_file
19from jutil.admin import admin_log
20from jutil.format import get_media_full_path, format_xml_bytes, camel_case_to_underscore
22logger = logging.getLogger(__name__)
25def etree_find_element(el: etree.Element, ns: str, tag: str) -> Optional[etree.Element]:
26 """
27 :param el: Root Element
28 :param ns: Target namespace
29 :param tag: Target tag
30 :return: Element if found
31 """
32 if not ns.startswith("{"):
33 ns = "{" + ns + "}"
34 els = list(el.iter("{}{}".format(ns, tag)))
35 if not els:
36 return None
37 if len(els) > 1:
38 return None
39 return els[0]
42def etree_get_element(el: etree.Element, ns: str, tag: str) -> etree.Element:
43 """
44 :param el: Root Element
45 :param ns: Target namespace
46 :param tag: Target tag
47 :return: Found Element
48 """
49 if not ns.startswith("{"):
50 ns = "{" + ns + "}"
51 els = list(el.iter("{}{}".format(ns, tag)))
52 if not els:
53 raise Exception("{} not found from {}".format(tag, el))
54 if len(els) > 1:
55 raise Exception("{} found from {} more than once".format(tag, el))
56 return els[0]
59def strip_xml_header_bytes(xml: bytes) -> bytes:
60 return b"\n".join(xml.split(b"\n")[1:])
63def generate_wspki_request( # pylint: disable=too-many-locals,too-many-statements,too-many-branches
64 soap_call: WsEdiSoapCall, payout_party: PayoutParty, **kwargs
65) -> bytes:
66 ws = soap_call.connection
67 command = soap_call.command
68 command_lower = command.lower()
70 if command_lower == "getcertificate":
71 soap_template_name = "jbank/pki_get_certificate_soap_template.xml"
72 else:
73 soap_template_name = "jbank/pki_soap_template.xml"
74 soap_body_bytes = ws.get_pki_template(soap_template_name, soap_call, **kwargs)
75 envelope = etree.fromstring(soap_body_bytes)
76 if "opc" in envelope.nsmap:
77 pkif_ns = "{" + envelope.nsmap["opc"] + "}"
78 elem_ns = pkif_ns
79 else:
80 for ns_name in ["elem", "pkif"]:
81 if ns_name not in envelope.nsmap:
82 raise Exception("WS-PKI {} SOAP template invalid, '{}' namespace missing".format(command, ns_name))
83 pkif_ns = "{" + envelope.nsmap["pkif"] + "}"
84 elem_ns = "{" + envelope.nsmap["elem"] + "}"
85 req_hdr_el = etree_get_element(envelope, pkif_ns, "RequestHeader")
86 cmd_el = req_hdr_el.getparent()
88 if command_lower in ["getbankcertificate"]:
89 if not ws.bank_root_cert_full_path:
90 raise Exception("Bank root certificate missing")
92 req_el = etree.SubElement(cmd_el, "{}{}Request".format(elem_ns, command))
93 cert = get_x509_cert_from_file(ws.bank_root_cert_full_path)
94 logger.info("BankRootCertificateSerialNo %s", cert.serial_number)
95 el = etree.SubElement(req_el, "{}BankRootCertificateSerialNo".format(elem_ns))
96 el.text = str(cert.serial_number)
97 el = etree.SubElement(req_el, "{}Timestamp".format(elem_ns))
98 el.text = soap_call.timestamp.isoformat()
99 el = etree.SubElement(req_el, "{}RequestId".format(elem_ns))
100 el.text = soap_call.request_identifier
102 elif command_lower in ["createcertificate", "renewcertificate", "getcertificate"]:
103 is_create = command_lower in ["createcertificate", "getcertificate"]
104 is_renew = command_lower == "renewcertificate"
105 is_encrypted = command_lower in ["createcertificate", "renewcertificate"]
106 template_name = "pki_" + camel_case_to_underscore(command) + "_request_template.xml"
108 if is_create:
109 encryption_pk = create_private_key()
110 signing_pk = create_private_key()
111 encryption_pk_pem = get_private_key_pem(encryption_pk)
112 signing_pk_pem = get_private_key_pem(signing_pk)
113 encryption_pk_filename = "certs/ws{}-{}-{}.pem".format(ws.id, soap_call.timestamp_digits, "EncryptionKey")
114 signing_pk_filename = "certs/ws{}-{}-{}.pem".format(ws.id, soap_call.timestamp_digits, "SigningKey")
115 ws.encryption_key_file.name = encryption_pk_filename
116 ws.signing_key_file.name = signing_pk_filename
117 ws.save()
118 admin_log(
119 [ws],
120 "Encryption and signing private keys set as {} and {}".format(
121 encryption_pk_filename, signing_pk_filename
122 ),
123 )
124 write_private_key_pem_file(get_media_full_path(encryption_pk_filename), encryption_pk_pem)
125 write_private_key_pem_file(get_media_full_path(signing_pk_filename), signing_pk_pem)
126 else:
127 encryption_pk = load_private_key_from_pem_file(ws.encryption_key_full_path)
128 signing_pk = load_private_key_from_pem_file(ws.signing_key_full_path)
130 csr_params = {
131 "common_name": payout_party.name,
132 "organization_name": payout_party.name,
133 "country_name": payout_party.country_code,
134 "organizational_unit_name": "IT-services",
135 "locality_name": "Helsinki",
136 "state_or_province_name": "Uusimaa",
137 "surname": ws.sender_identifier,
138 }
139 encryption_csr = create_csr_pem(encryption_pk, **csr_params)
140 logger.info("encryption_csr: %s", encryption_csr)
141 signing_csr = create_csr_pem(signing_pk, **csr_params)
142 logger.info("signing_csr: %s", signing_csr)
143 req = ws.get_pki_template(
144 "jbank/" + template_name,
145 soap_call,
146 **{
147 "encryption_cert_pkcs10": strip_pem_header_and_footer(encryption_csr).decode().replace("\n", ""),
148 "signing_cert_pkcs10": strip_pem_header_and_footer(signing_csr).decode().replace("\n", ""),
149 "old_signing_cert": ws.signing_cert if is_renew else None,
150 }
151 )
152 logger.info("%s request:\n%s", command, format_xml_bytes(req).decode())
154 if is_renew:
155 req = ws.sign_pki_request(req)
156 logger.info("%s request signed:\n%s", command, format_xml_bytes(req).decode())
158 if is_encrypted:
159 logger.debug("Encrypting PKI request...")
160 enc_req_bytes = ws.encrypt_pki_request(req)
161 logger.info("%s request encrypted:\n%s", command, format_xml_bytes(enc_req_bytes).decode())
162 req_el = etree.fromstring(enc_req_bytes)
163 cmd_el.insert(cmd_el.index(req_hdr_el) + 1, req_el)
164 else:
165 logger.debug("Base64 encoding PKI request...")
166 req_b64 = base64.encodebytes(req)
167 req_el = etree.SubElement(cmd_el, "{}ApplicationRequest".format(elem_ns))
168 req_el.text = req_b64
170 elif command_lower in ["certificatestatus", "getowncertificatelist"]:
171 cert = get_x509_cert_from_file(ws.signing_cert_full_path)
172 req = ws.get_pki_template(
173 "jbank/pki_certificate_status_request_template.xml",
174 soap_call,
175 **{
176 "certs": [cert],
177 }
178 )
179 logger.info("%s request:\n%s", command, format_xml_bytes(req).decode())
181 req = ws.sign_pki_request(req)
182 logger.info("%s request signed:\n%s", command, format_xml_bytes(req).decode())
183 req_el = etree.fromstring(req)
184 cmd_el.insert(cmd_el.index(req_hdr_el) + 1, req_el)
186 else:
187 raise Exception("{} not implemented".format(command))
189 body_bytes = etree.tostring(envelope)
190 return body_bytes
193def process_wspki_response(content: bytes, soap_call: WsEdiSoapCall): # noqa
194 ws = soap_call.connection
195 command = soap_call.command
196 command_lower = command.lower()
197 envelope = etree.fromstring(content)
199 # check for errors
200 return_code: str = ""
201 return_text: str = ""
202 for el in envelope.iter():
203 # print(el.tag)
204 if el.tag and el.tag.endswith("}ResponseCode"):
205 return_code = el.text
206 return_text_el = list(envelope.iter(el.tag[:-4] + "Text"))[0]
207 return_text = return_text_el.text if return_text_el is not None else ""
208 if return_code not in ["00", "0"]:
209 raise Exception("WS-PKI {} call failed, ReturnCode {} ({})".format(command, return_code, return_text))
211 # find namespaces
212 pkif_ns = ""
213 elem_ns = ""
214 for ns_name, ns_url in envelope.nsmap.items():
215 assert isinstance(ns_name, str)
216 if ns_url.endswith("PKIFactoryService/elements"):
217 elem_ns = "{" + ns_url + "}"
218 elif ns_url.endswith("PKIFactoryService"):
219 pkif_ns = "{" + ns_url + "}"
220 elif ns_url.endswith("OPCertificateService"):
221 pkif_ns = "{" + ns_url + "}"
222 elem_ns = "{http://op.fi/mlp/xmldata/}"
223 if not pkif_ns:
224 raise Exception("WS-PKI {} SOAP response invalid, PKIFactoryService namespace missing".format(command))
225 if not elem_ns:
226 raise Exception("WS-PKI {} SOAP response invalid, PKIFactoryService/elements namespace missing".format(command))
228 if command_lower == "getbankcertificate":
229 res_el = etree_get_element(envelope, elem_ns, command + "Response")
230 for cert_name in ["BankEncryptionCert", "BankSigningCert", "BankRootCert"]:
231 data_base64 = etree_get_element(res_el, elem_ns, cert_name).text
232 filename = "certs/ws{}-{}-{}.pem".format(ws.id, soap_call.timestamp_digits, cert_name)
233 write_cert_pem_file(get_media_full_path(filename), data_base64.encode())
234 if cert_name == "BankEncryptionCert":
235 ws.bank_encryption_cert_file.name = filename
236 elif cert_name == "BankSigningCert":
237 ws.bank_signing_cert_file.name = filename
238 elif cert_name == "BankRootCert":
239 ws.bank_root_cert_file.name = filename
240 ws.save()
241 admin_log([ws], "{} set by system from SOAP call response id={}".format(cert_name, soap_call.id))
243 elif command_lower in ["createcertificate", "renewcertificate"]:
244 res_el = etree_get_element(envelope, elem_ns, command + "Response")
245 for cert_name in ["EncryptionCert", "SigningCert", "CACert"]:
246 data_base64 = etree_get_element(res_el, elem_ns, cert_name).text
247 filename = "certs/ws{}-{}-{}.pem".format(ws.id, soap_call.timestamp_digits, cert_name)
248 write_cert_pem_file(get_media_full_path(filename), data_base64.encode())
249 if cert_name == "EncryptionCert":
250 ws.encryption_cert_file.name = filename
251 admin_log([ws], "soap_call(id={}): encryption_cert_file={}".format(soap_call.id, filename))
252 elif cert_name == "SigningCert":
253 ws.signing_cert_file.name = filename
254 admin_log([ws], "soap_call(id={}): signing_cert_file={}".format(soap_call.id, filename))
255 elif cert_name == "CACert":
256 ws.ca_cert_file.name = filename
257 admin_log([ws], "soap_call(id={}): ca_cert_file={}".format(soap_call.id, filename))
258 ws.save()
260 elif command_lower in ["getcertificate"]:
261 app_res = envelope.find(
262 "{http://schemas.xmlsoap.org/soap/envelope/}Body/{http://mlp.op.fi/OPCertificateService}getCertificateout/{http://mlp.op.fi/OPCertificateService}ApplicationResponse" # noqa
263 )
264 if app_res is None:
265 raise Exception("{} not found from {}".format("ApplicationResponse", envelope))
266 data_base64 = base64.decodebytes(str(app_res.text).encode())
267 cert_app_res = etree.fromstring(data_base64)
268 if cert_app_res is None:
269 raise Exception("Failed to create XML document from decoded ApplicationResponse")
270 cert_el = cert_app_res.find(
271 "./{http://op.fi/mlp/xmldata/}Certificates/{http://op.fi/mlp/xmldata/}Certificate/{http://op.fi/mlp/xmldata/}Certificate"
272 )
273 if cert_el is None:
274 raise Exception("{} not found from {}".format("Certificate", cert_app_res))
275 cert_bytes = base64.decodebytes(str(cert_el.text).encode())
276 cert_name = "SigningCert"
277 filename = "certs/ws{}-{}-{}.pem".format(ws.id, soap_call.timestamp_digits, cert_name)
278 cert_full_path = get_media_full_path(filename)
279 with open(cert_full_path, "wb") as fp:
280 fp.write(cert_bytes)
281 logger.info("%s written", cert_full_path)
282 ws.signing_cert_file.name = filename
283 admin_log([ws], "soap_call(id={}): signing_cert_file={}".format(soap_call.id, filename))
284 ws.save()
286 elif command_lower in ["createcertificate", "renewcertificate"]:
287 res_el = etree_get_element(envelope, elem_ns, command + "Response")
288 for cert_name in ["EncryptionCert", "SigningCert", "CACert"]:
289 data_base64 = etree_get_element(res_el, elem_ns, cert_name).text
290 filename = "certs/ws{}-{}-{}.pem".format(ws.id, soap_call.timestamp_digits, cert_name)
291 write_cert_pem_file(get_media_full_path(filename), data_base64.encode())
292 if cert_name == "EncryptionCert":
293 ws.encryption_cert_file.name = filename
294 admin_log([ws], "soap_call(id={}): encryption_cert_file={}".format(soap_call.id, filename))
295 elif cert_name == "SigningCert":
296 ws.signing_cert_file.name = filename
297 admin_log([ws], "soap_call(id={}): signing_cert_file={}".format(soap_call.id, filename))
298 elif cert_name == "CACert":
299 ws.ca_cert_file.name = filename
300 admin_log([ws], "soap_call(id={}): ca_cert_file={}".format(soap_call.id, filename))
301 ws.save()
303 elif command_lower in ["certificatestatus", "getowncertificatelist"]:
304 pass
306 else:
307 raise Exception("{} not implemented".format(command))
310def wspki_execute(
311 ws: WsEdiConnection, payout_party: PayoutParty, command: str, verbose: bool = False, **kwargs
312) -> bytes:
313 """
314 :param ws:
315 :param payout_party:
316 :param command:
317 :param verbose:
318 :return: str
319 """
320 if ws and not ws.enabled:
321 raise Exception(_("ws.edi.connection.not.enabled").format(ws=ws))
323 soap_call = WsEdiSoapCall(connection=ws, command=command, **kwargs)
324 soap_call.full_clean()
325 soap_call.save()
326 call_str = "WsEdiSoapCall({})".format(soap_call.id)
327 try:
328 http_headers = {
329 "Connection": "Close",
330 "Content-Type": "text/xml",
331 # "Content-Type": 'application/soap+xml;charset=UTF8;action="{}"'.format(command),
332 "Method": "POST",
333 "SOAPAction": "",
334 # "SOAPAction": '"{}"'.format(command),
335 "User-Agent": "Kajala WS",
336 }
338 body_bytes: bytes = generate_wspki_request(soap_call, payout_party, **kwargs)
339 if verbose:
340 logger.info(
341 "------------------------------------------------------ %s http_headers\n%s",
342 call_str,
343 "\n".join(["{}: {}".format(k, v) for k, v in http_headers.items()]),
344 )
345 logger.info(
346 "------------------------------------------------------ %s body_bytes\n%s",
347 call_str,
348 body_bytes.decode(),
349 )
350 debug_output = command in ws.debug_command_list or "ALL" in ws.debug_command_list
351 if debug_output and soap_call.debug_request_full_path:
352 with open(soap_call.debug_request_full_path, "wb") as fp:
353 fp.write(body_bytes)
355 res = requests.post(ws.pki_endpoint, data=body_bytes, headers=http_headers)
356 if verbose:
357 logger.info(
358 "------------------------------------------------------ %s HTTP response %s\n%s",
359 call_str,
360 res.status_code,
361 format_xml_bytes(res.content).decode(),
362 )
363 if debug_output and soap_call.debug_response_full_path:
364 with open(soap_call.debug_response_full_path, "wb") as fp:
365 fp.write(res.content)
366 if res.status_code >= 300:
367 logger.error(
368 "------------------------------------------------------ %s HTTP response %s\n%s",
369 call_str,
370 res.status_code,
371 format_xml_bytes(res.content).decode(),
372 )
373 raise Exception("WS-PKI {} HTTP {}".format(command, res.status_code))
375 process_wspki_response(res.content, soap_call)
377 soap_call.executed = now()
378 soap_call.save(update_fields=["executed"])
379 return res.content
380 except Exception:
381 soap_call.error = traceback.format_exc()
382 soap_call.save(update_fields=["error"])
383 raise