Coverage for jbank/wspki.py: 0%

# Coverage for jbank/wspki.py : 0%

## 249 statements

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

1 import base64

2 import logging

3 import traceback

4 from typing import Optional

5 import requests

6 from django.utils.timezone import now

7 from django.utils.translation import ugettext as _

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

16 from jbank.models import WsEdiConnection, WsEdiSoapCall, PayoutParty

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

18 from jbank.x509_helpers import get_x509_cert_from_file, write_cert_pem_file

19 from jutil.admin import admin_log

20 from jutil.format import get_media_full_path, format_xml_bytes, camel_case_to_underscore

21

22 logger = logging.getLogger(__name__)

23

24

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

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

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

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

61

62

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

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

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