Coverage for jbank/wspki.py: 0%

# Coverage for jbank/wspki.py : 0%

## 189 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 logging

2 import traceback

3 from typing import Callable, Optional

4 import requests

5 from django.utils.timezone import now

6 from django.utils.translation import ugettext as _

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

9 from jbank.models import WsEdiConnection, WsEdiSoapCall, PayoutParty

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

11 from jbank.x509_helpers import get_x509_cert_from_file, write_cert_pem_file

12 from jutil.admin import admin_log

13 from jutil.format import get_media_full_path, format_xml_bytes

14

15

16 logger = logging.getLogger(__name__)

17

18

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

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

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

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

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