Coverage for jbank/wspki.py: 0%

1# pylint: disable=c-extension-no-member

2 import base64

3 import logging

4 import traceback

5 from typing import Optional

6 import requests

7 from django.utils.timezone import now

8 from django.utils.translation import gettext as _

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

17 from jbank.models import WsEdiConnection, WsEdiSoapCall, PayoutParty

18 from lxml import etree # type: ignore # pytype: disable=import-error

19 from jbank.x509_helpers import get_x509_cert_from_file, write_cert_pem_file

20 from jutil.admin import admin_log

21 from jutil.format import get_media_full_path, format_xml_bytes, camel_case_to_underscore

22

23 logger = logging.getLogger(__name__)

24

25

26 def 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

43 def 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

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

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

62

63

64 def 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

200 def 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

312 def 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