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