Hide keyboard shortcuts

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 

21 

22logger = logging.getLogger(__name__) 

23 

24 

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] 

40 

41 

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] 

57 

58 

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

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

61 

62 

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

69 

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

87 

88 if command_lower in ["getbankcertificate"]: 

89 if not ws.bank_root_cert_full_path: 

90 raise Exception("Bank root certificate missing") 

91 

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 

101 

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" 

107 

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) 

129 

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

153 

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

157 

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 

169 

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

180 

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) 

185 

186 else: 

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

188 

189 body_bytes = etree.tostring(envelope) 

190 return body_bytes 

191 

192 

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) 

198 

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

210 

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

227 

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

242 

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

259 

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

285 

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

302 

303 elif command_lower in ["certificatestatus", "getowncertificatelist"]: 

304 pass 

305 

306 else: 

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

308 

309 

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

322 

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 } 

337 

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) 

354 

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

374 

375 process_wspki_response(res.content, soap_call) 

376 

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