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 logging 

2import traceback 

3from typing import Callable, Optional 

4import requests 

5from django.utils.timezone import now 

6from django.utils.translation import ugettext as _ 

7from jbank.csr_helpers import create_private_key, get_private_key_pem, strip_pem_header_and_footer, create_csr_pem, \ 

8 write_private_key_pem_file, load_private_key_from_pem_file 

9from jbank.models import WsEdiConnection, WsEdiSoapCall, PayoutParty 

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

11from jbank.x509_helpers import get_x509_cert_from_file, write_cert_pem_file 

12from jutil.admin import admin_log 

13from jutil.format import get_media_full_path, format_xml_bytes 

14 

15 

16logger = logging.getLogger(__name__) 

17 

18 

19def etree_find_element(el: etree.Element, ns: str, tag: str) -> Optional[etree.Element]: 

20 """ 

21 :param el: Root Element 

22 :param ns: Target namespace 

23 :param tag: Target tag 

24 :return: Element if found 

25 """ 

26 if not ns.startswith('{'): 

27 ns = '{' + ns + '}' 

28 els = list(el.iter('{}{}'.format(ns, tag))) 

29 if not els: 

30 return None 

31 if len(els) > 1: 

32 return None 

33 return els[0] 

34 

35 

36def etree_get_element(el: etree.Element, ns: str, tag: str) -> etree.Element: 

37 """ 

38 :param el: Root Element 

39 :param ns: Target namespace 

40 :param tag: Target tag 

41 :return: Found Element 

42 """ 

43 if not ns.startswith('{'): 

44 ns = '{' + ns + '}' 

45 els = list(el.iter('{}{}'.format(ns, tag))) 

46 if not els: 

47 raise Exception('{} not found from {}'.format(tag, el)) 

48 if len(els) > 1: 

49 raise Exception('{} found from {} more than once'.format(tag, el)) 

50 return els[0] 

51 

52 

53def generate_wspki_request(soap_call: WsEdiSoapCall, payout_party: PayoutParty, **kwargs) -> bytes: # pylint: disable=too-many-locals,too-many-statements 

54 ws = soap_call.connection 

55 command = soap_call.command 

56 

57 soap_body_bytes = ws.get_pki_template('jbank/pki_soap_template.xml', soap_call, **kwargs) 

58 envelope = etree.fromstring(soap_body_bytes) 

59 for ns_name in ['elem', 'pkif']: 

60 if ns_name not in envelope.nsmap: 

61 raise Exception("WS-PKI {} SOAP template invalid, '{}' namespace missing".format(command, ns_name)) 

62 pkif_ns = '{' + envelope.nsmap['pkif'] + '}' 

63 elem_ns = '{' + envelope.nsmap['elem'] + '}' 

64 req_hdr_el = etree_get_element(envelope, pkif_ns, 'RequestHeader') 

65 cmd_el = req_hdr_el.getparent() 

66 

67 if command == 'GetBankCertificate': 

68 if not ws.bank_root_cert_full_path: 

69 raise Exception('Bank root certificate missing') 

70 

71 req_el = etree.SubElement(cmd_el, '{}{}Request'.format(elem_ns, command)) 

72 cert = get_x509_cert_from_file(ws.bank_root_cert_full_path) 

73 logger.info('BankRootCertificateSerialNo %s', cert.serial_number) 

74 el = etree.SubElement(req_el, '{}BankRootCertificateSerialNo'.format(elem_ns)) 

75 el.text = str(cert.serial_number) 

76 el = etree.SubElement(req_el, '{}Timestamp'.format(elem_ns)) 

77 el.text = soap_call.timestamp.isoformat() 

78 el = etree.SubElement(req_el, '{}RequestId'.format(elem_ns)) 

79 el.text = soap_call.request_identifier 

80 

81 elif command in ['CreateCertificate', 'RenewCertificate']: 

82 is_create = command == 'CreateCertificate' 

83 is_renew = command == 'RenewCertificate' 

84 

85 if is_create: 

86 encryption_pk = create_private_key() 

87 signing_pk = create_private_key() 

88 encryption_pk_pem = get_private_key_pem(encryption_pk) 

89 signing_pk_pem = get_private_key_pem(signing_pk) 

90 encryption_pk_filename = 'certs/ws{}-{}-{}.pem'.format(ws.id, soap_call.timestamp_digits, 'EncryptionKey') 

91 signing_pk_filename = 'certs/ws{}-{}-{}.pem'.format(ws.id, soap_call.timestamp_digits, 'SigningKey') 

92 ws.encryption_key_file.name = encryption_pk_filename 

93 ws.signing_key_file.name = signing_pk_filename 

94 ws.save() 

95 admin_log([ws], 'Encryption and signing private keys set as {} and {}'.format(encryption_pk_filename, signing_pk_filename)) 

96 write_private_key_pem_file(get_media_full_path(encryption_pk_filename), encryption_pk_pem) 

97 write_private_key_pem_file(get_media_full_path(signing_pk_filename), signing_pk_pem) 

98 else: 

99 encryption_pk = load_private_key_from_pem_file(ws.encryption_key_full_path) 

100 signing_pk = load_private_key_from_pem_file(ws.signing_key_full_path) 

101 

102 csr_params = { 

103 'common_name': payout_party.name, 

104 'organization_name': payout_party.name, 

105 'country_name': payout_party.country_code, 

106 'organizational_unit_name': 'IT-services', 

107 'locality_name': 'Helsinki', 

108 'state_or_province_name': 'Uusimaa', 

109 } 

110 encryption_csr = create_csr_pem(encryption_pk, **csr_params) 

111 signing_csr = create_csr_pem(signing_pk, **csr_params) 

112 req = ws.get_pki_template('jbank/pki_create_certificate_request_template.xml', soap_call, **{ 

113 'encryption_cert_pkcs10': strip_pem_header_and_footer(encryption_csr).decode().replace('\n', ''), 

114 'signing_cert_pkcs10': strip_pem_header_and_footer(signing_csr).decode().replace('\n', ''), 

115 'old_signing_cert': ws.signing_cert if is_renew else None, 

116 }) 

117 logger.info('%s request:\n%s', command, format_xml_bytes(req).decode()) 

118 

119 if is_renew: 

120 req = ws.sign_pki_request(req) 

121 logger.info('%s request signed:\n%s', command, format_xml_bytes(req).decode()) 

122 

123 enc_req_bytes = ws.encrypt_pki_request(req) 

124 logger.info('%s request encrypted:\n%s', command, format_xml_bytes(enc_req_bytes).decode()) 

125 req_el = etree.fromstring(enc_req_bytes) 

126 

127 cmd_el.insert(cmd_el.index(req_hdr_el)+1, req_el) 

128 

129 elif command in ['CertificateStatus', 'GetOwnCertificateList']: 

130 cert = get_x509_cert_from_file(ws.signing_cert_full_path) 

131 req = ws.get_pki_template('jbank/pki_certificate_status_request_template.xml', soap_call, **{ 

132 'certs': [cert], 

133 }) 

134 logger.info('%s request:\n%s', command, format_xml_bytes(req).decode()) 

135 

136 req = ws.sign_pki_request(req) 

137 logger.info('%s request signed:\n%s', command, format_xml_bytes(req).decode()) 

138 req_el = etree.fromstring(req) 

139 cmd_el.insert(cmd_el.index(req_hdr_el)+1, req_el) 

140 

141 else: 

142 raise Exception('{} not implemented'.format(command)) 

143 

144 body_bytes = etree.tostring(envelope) 

145 return body_bytes 

146 

147 

148def process_wspki_response(content: bytes, soap_call: WsEdiSoapCall): # noqa 

149 ws = soap_call.connection 

150 command = soap_call.command 

151 envelope = etree.fromstring(content) 

152 

153 # check for errors 

154 return_code: str = '' 

155 return_text: str = '' 

156 for el in envelope.iter(): 

157 if el.tag and el.tag.endswith('}ReturnCode'): 

158 return_code = el.text 

159 return_text_el = list(envelope.iter(el.tag[:-4] + 'Text'))[0] 

160 return_text = return_text_el.text if return_text_el is not None else '' 

161 if return_code != '00': 

162 raise Exception("WS-PKI {} call failed, ReturnCode {} ({})".format(command, return_code, return_text)) 

163 

164 # find namespaces 

165 pkif_ns = '' 

166 elem_ns = '' 

167 for ns_name, ns_url in envelope.nsmap.items(): 

168 assert isinstance(ns_name, str) 

169 if ns_url.endswith('PKIFactoryService/elements'): 

170 elem_ns = '{' + ns_url + '}' 

171 elif ns_url.endswith('PKIFactoryService'): 

172 pkif_ns = '{' + ns_url + '}' 

173 if not pkif_ns: 

174 raise Exception("WS-PKI {} SOAP response invalid, PKIFactoryService namespace missing".format(command)) 

175 if not elem_ns: 

176 raise Exception("WS-PKI {} SOAP response invalid, PKIFactoryService/elements namespace missing".format(command)) 

177 

178 # find response element 

179 res_el = etree_get_element(envelope, elem_ns, command + 'Response') 

180 

181 if command == 'GetBankCertificate': 

182 for cert_name in ['BankEncryptionCert', 'BankSigningCert', 'BankRootCert']: 

183 data_base64 = etree_get_element(res_el, elem_ns, cert_name).text 

184 filename = 'certs/ws{}-{}-{}.pem'.format(ws.id, soap_call.timestamp_digits, cert_name) 

185 write_cert_pem_file(get_media_full_path(filename), data_base64.encode()) 

186 if cert_name == 'BankEncryptionCert': 

187 ws.bank_encryption_cert_file.name = filename 

188 elif cert_name == 'BankSigningCert': 

189 ws.bank_signing_cert_file.name = filename 

190 elif cert_name == 'BankRootCert': 

191 ws.bank_root_cert_file.name = filename 

192 ws.save() 

193 admin_log([ws], '{} set by system from SOAP call response id={}'.format(cert_name, soap_call.id)) 

194 

195 elif command in ['CreateCertificate', 'RenewCertificate']: 

196 for cert_name in ['EncryptionCert', 'SigningCert', 'CACert']: 

197 data_base64 = etree_get_element(res_el, elem_ns, cert_name).text 

198 filename = 'certs/ws{}-{}-{}.pem'.format(ws.id, soap_call.timestamp_digits, cert_name) 

199 write_cert_pem_file(get_media_full_path(filename), data_base64.encode()) 

200 if cert_name == 'EncryptionCert': 

201 ws.encryption_cert_file.name = filename 

202 admin_log([ws], 'soap_call(id={}): encryption_cert_file={}'.format(soap_call.id, filename)) 

203 elif cert_name == 'SigningCert': 

204 ws.signing_cert_file.name = filename 

205 admin_log([ws], 'soap_call(id={}): signing_cert_file={}'.format(soap_call.id, filename)) 

206 elif cert_name == 'CACert': 

207 ws.ca_cert_file.name = filename 

208 admin_log([ws], 'soap_call(id={}): ca_cert_file={}'.format(soap_call.id, filename)) 

209 ws.save() 

210 

211 elif command in ['CertificateStatus', 'GetOwnCertificateList']: 

212 pass 

213 

214 else: 

215 raise Exception('{} not implemented'.format(command)) 

216 

217 

218def wspki_execute(ws: WsEdiConnection, payout_party: PayoutParty, command: str, 

219 verbose: bool = False, cls: Callable = WsEdiSoapCall, **kwargs) -> bytes: 

220 """ 

221 :param ws: 

222 :param payout_party: 

223 :param command: 

224 :param verbose: 

225 :param cls: 

226 :return: str 

227 """ 

228 if ws and not ws.enabled: 

229 raise Exception(_('ws.edi.connection.not.enabled').format(ws=ws)) 

230 

231 soap_call = cls(connection=ws, command=command, **kwargs) 

232 assert isinstance(soap_call, WsEdiSoapCall) 

233 soap_call.full_clean() 

234 soap_call.save() 

235 call_str = 'WsEdiSoapCall({})'.format(soap_call.id) 

236 try: 

237 body_bytes: bytes = generate_wspki_request(soap_call, payout_party, **kwargs) 

238 if verbose: 

239 logger.info('------------------------------------------------------ %s body_bytes\n%s', call_str, body_bytes.decode()) 

240 debug_output = command in ws.debug_command_list or 'ALL' in ws.debug_command_list 

241 if debug_output: 

242 with open(soap_call.debug_request_full_path, 'wb') as fp: 

243 fp.write(body_bytes) 

244 

245 http_headers = { 

246 'Connection': 'Close', 

247 'Content-Type': 'text/xml', 

248 'Method': 'POST', 

249 'SOAPAction': '', 

250 'User-Agent': 'Kajala WS', 

251 } 

252 if verbose: 

253 logger.info('HTTP POST %s', ws.pki_endpoint) 

254 res = requests.post(ws.pki_endpoint, data=body_bytes, headers=http_headers) 

255 if debug_output: 

256 with open(soap_call.debug_response_full_path, 'wb') as fp: 

257 fp.write(res.content) 

258 if verbose: 

259 logger.info('------------------------------------------------------ %s HTTP response %s\n%s', call_str, 

260 res.status_code, format_xml_bytes(res.content).decode()) 

261 if res.status_code >= 300: 

262 logger.error('------------------------------------------------------ %s HTTP response %s\n%s', call_str, 

263 res.status_code, format_xml_bytes(res.content).decode()) 

264 raise Exception("WS-PKI {} HTTP {}".format(command, res.status_code)) 

265 

266 process_wspki_response(res.content, soap_call) 

267 

268 soap_call.executed = now() 

269 soap_call.save(update_fields=['executed']) 

270 return res.content 

271 except Exception: 

272 soap_call.error = traceback.format_exc() 

273 soap_call.save(update_fields=['error']) 

274 raise