Coverage for jbank/wspki.py: 0%

257 statements  

« 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 

22 

23logger = logging.getLogger(__name__) 

24 

25 

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] 

41 

42 

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] 

58 

59 

60def strip_xml_header_bytes(xml: bytes) -> bytes: 

61 return b"\n".join(xml.split(b"\n")[1:]) 

62 

63 

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() 

70 

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() 

88 

89 if command_lower in ["getbankcertificate"]: 

90 if not ws.bank_root_cert_full_path: 

91 raise Exception("Bank root certificate missing") 

92 

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 

102 

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" 

115 

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) 

135 

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()) 

160 

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()) 

164 

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 

176 

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()) 

187 

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) 

192 

193 else: 

194 raise Exception("{} not implemented".format(command)) 

195 

196 body_bytes = etree.tostring(envelope) 

197 return body_bytes 

198 

199 

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) 

205 

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)) 

217 

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)) 

234 

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)) 

249 

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() 

266 

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() 

283 

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() 

307 

308 else: 

309 raise Exception("{} unsupported".format(command)) 

310 

311 

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)) 

333 

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 } 

346 

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) 

365 

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)) 

383 

384 process_wspki_response(res.content, soap_call) 

385 

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