txhttputil 1.4.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- txhttputil/__init__.py +9 -0
- txhttputil/downloader/HttpFileDownloader.py +102 -0
- txhttputil/downloader/HttpResourceProxy.py +151 -0
- txhttputil/downloader/__init__.py +8 -0
- txhttputil/login_page/LoginElement.py +45 -0
- txhttputil/login_page/LoginTemplate.xml +63 -0
- txhttputil/login_page/__init__.py +8 -0
- txhttputil/site/AuthCredentials.py +38 -0
- txhttputil/site/AuthResource.py +58 -0
- txhttputil/site/AuthSessionWrapper.py +224 -0
- txhttputil/site/AuthUserDetails.py +51 -0
- txhttputil/site/BasicResource.py +155 -0
- txhttputil/site/BasicResourceTest.py +112 -0
- txhttputil/site/FileUnderlayResource.py +188 -0
- txhttputil/site/FileUnderlayResourceTest.py +108 -0
- txhttputil/site/FileUploadRequest.py +250 -0
- txhttputil/site/RedirectToHttpsResource.py +43 -0
- txhttputil/site/RedirectionRule.py +24 -0
- txhttputil/site/ResourceCreator.py +12 -0
- txhttputil/site/RootResource.py +29 -0
- txhttputil/site/RootSiteTest.py +8 -0
- txhttputil/site/SiteUtil.py +157 -0
- txhttputil/site/StaticFileResource.py +163 -0
- txhttputil/site/__init__.py +8 -0
- txhttputil/util/DeferUtil.py +54 -0
- txhttputil/util/ModuleUtil.py +23 -0
- txhttputil/util/PemUtil.py +750 -0
- txhttputil/util/SslUtil.py +396 -0
- txhttputil/util/__init__.py +8 -0
- txhttputil-1.4.9.dist-info/LICENSE +22 -0
- txhttputil-1.4.9.dist-info/METADATA +82 -0
- txhttputil-1.4.9.dist-info/RECORD +34 -0
- txhttputil-1.4.9.dist-info/WHEEL +5 -0
- txhttputil-1.4.9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
# Copyright (c) 2016-2026 Flying Ion Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# This software is open source; the MIT license applies.
|
|
4
|
+
# See LICENSE for the full license text.
|
|
5
|
+
#
|
|
6
|
+
# Website : https://flyingion.com
|
|
7
|
+
# Support : contact@flyingion.com
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import textwrap
|
|
11
|
+
from collections import namedtuple
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from functools import partial
|
|
14
|
+
from itertools import chain
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import List, Dict, Literal
|
|
17
|
+
|
|
18
|
+
import pem
|
|
19
|
+
from OpenSSL.crypto import (
|
|
20
|
+
load_certificate,
|
|
21
|
+
FILETYPE_PEM,
|
|
22
|
+
X509Store,
|
|
23
|
+
X509StoreContext,
|
|
24
|
+
X509StoreContextError,
|
|
25
|
+
)
|
|
26
|
+
from cryptography.hazmat._oid import NameOID
|
|
27
|
+
from cryptography.x509.base import Version
|
|
28
|
+
from cryptography.x509.oid import ExtendedKeyUsageOID
|
|
29
|
+
from cryptography.hazmat.primitives.asymmetric import dh
|
|
30
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
31
|
+
load_pem_private_key,
|
|
32
|
+
Encoding,
|
|
33
|
+
ParameterFormat,
|
|
34
|
+
load_pem_parameters,
|
|
35
|
+
)
|
|
36
|
+
from cryptography.x509 import (
|
|
37
|
+
Certificate,
|
|
38
|
+
load_pem_x509_certificate,
|
|
39
|
+
BasicConstraints,
|
|
40
|
+
ExtensionNotFound,
|
|
41
|
+
Extension,
|
|
42
|
+
ExtendedKeyUsage,
|
|
43
|
+
)
|
|
44
|
+
from pem import AbstractPEMObject
|
|
45
|
+
from treelib import Tree
|
|
46
|
+
from twisted.internet import ssl
|
|
47
|
+
from twisted.internet._sslverify import (
|
|
48
|
+
Certificate as TxCertificate,
|
|
49
|
+
OpenSSLDiffieHellmanParameters,
|
|
50
|
+
)
|
|
51
|
+
from twisted.python.filepath import FilePath
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PEM_CHECK_TYPE:
|
|
57
|
+
CHECK_TYPE_SSL_SERVER_IDENTITY = 0
|
|
58
|
+
CHECK_TYPE_SSL_CLIENT_IDENTITY = 1
|
|
59
|
+
CHECK_TYPE_SSL_MTLS_CA = 2
|
|
60
|
+
CHECK_TYPE_SSL_MTLS_PEER = 3
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
PemSimpleBank = namedtuple(
|
|
64
|
+
"PemSimpleBank",
|
|
65
|
+
[
|
|
66
|
+
"privateKeys",
|
|
67
|
+
"certificates",
|
|
68
|
+
"rootCAs",
|
|
69
|
+
"intermediateCAs",
|
|
70
|
+
"bundleFilePath",
|
|
71
|
+
],
|
|
72
|
+
defaults=([], [], [], [], ""),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PemBank:
|
|
77
|
+
MAX_COUNT = 99
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
pemSimpleBank: PemSimpleBank,
|
|
82
|
+
pemMap: Dict[AbstractPEMObject, Certificate],
|
|
83
|
+
):
|
|
84
|
+
self._pemSimpleBank = pemSimpleBank
|
|
85
|
+
self._pemMap = pemMap
|
|
86
|
+
self._trustTree = Tree()
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def trustChains(self) -> List[List[str]]:
|
|
90
|
+
# chains in x509 common names
|
|
91
|
+
return self._trustTree.paths_to_leaves()
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def privateKeys(self):
|
|
95
|
+
return self._pemSimpleBank.privateKeys
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def certificates(self):
|
|
99
|
+
return self._pemSimpleBank.certificates
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def rootCAs(self):
|
|
103
|
+
return self._pemSimpleBank.rootCAs
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def intermediateCAs(self):
|
|
107
|
+
return self._pemSimpleBank.intermediateCAs
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def bundleFilePath(self):
|
|
111
|
+
return self._pemSimpleBank.bundleFilePath
|
|
112
|
+
|
|
113
|
+
def check(
|
|
114
|
+
self,
|
|
115
|
+
checkType: PEM_CHECK_TYPE,
|
|
116
|
+
showCheck: bool = True,
|
|
117
|
+
targetHostname: str = None,
|
|
118
|
+
) -> bool:
|
|
119
|
+
if checkType in [
|
|
120
|
+
PEM_CHECK_TYPE.CHECK_TYPE_SSL_SERVER_IDENTITY,
|
|
121
|
+
PEM_CHECK_TYPE.CHECK_TYPE_SSL_CLIENT_IDENTITY,
|
|
122
|
+
]:
|
|
123
|
+
result = []
|
|
124
|
+
result.append(self._checkExtendedKeyUsage(checkType))
|
|
125
|
+
result.append(self._checkTrustChain(showCheck))
|
|
126
|
+
|
|
127
|
+
if False in result:
|
|
128
|
+
return False
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
if checkType == PEM_CHECK_TYPE.CHECK_TYPE_SSL_MTLS_CA:
|
|
132
|
+
return self._checkNumberLimits(
|
|
133
|
+
maxCertificateCount=0,
|
|
134
|
+
maxPrivateKeyCount=0,
|
|
135
|
+
maxRootCACount=self.MAX_COUNT,
|
|
136
|
+
maxIntermediateCACount=self.MAX_COUNT,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if checkType == PEM_CHECK_TYPE.CHECK_TYPE_SSL_MTLS_PEER:
|
|
140
|
+
return self._checkNumberLimits(
|
|
141
|
+
maxCertificateCount=self.MAX_COUNT,
|
|
142
|
+
maxPrivateKeyCount=0,
|
|
143
|
+
maxRootCACount=0,
|
|
144
|
+
maxIntermediateCACount=0,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _checkExtendedKeyUsage(
|
|
148
|
+
self,
|
|
149
|
+
usageType: Literal[
|
|
150
|
+
PEM_CHECK_TYPE.CHECK_TYPE_SSL_SERVER_IDENTITY,
|
|
151
|
+
PEM_CHECK_TYPE.CHECK_TYPE_SSL_CLIENT_IDENTITY,
|
|
152
|
+
],
|
|
153
|
+
) -> bool:
|
|
154
|
+
certificate: AbstractPEMObject = self._pemSimpleBank.certificates[0]
|
|
155
|
+
certificateParsed: Certificate = self._pemMap[certificate]
|
|
156
|
+
|
|
157
|
+
x509Version = X509Util.getX509Version(certificateParsed)
|
|
158
|
+
if x509Version != Version.v3.name:
|
|
159
|
+
logger.error(
|
|
160
|
+
f"The server certificate from bundle "
|
|
161
|
+
f"{self._pemSimpleBank.bundleFilePath} should be in "
|
|
162
|
+
f"version x509 v3, got '{x509Version}'"
|
|
163
|
+
)
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
usages = X509Util.getExtendedKeyUsages(certificateParsed)
|
|
167
|
+
|
|
168
|
+
if usageType == PEM_CHECK_TYPE.CHECK_TYPE_SSL_SERVER_IDENTITY:
|
|
169
|
+
success = ExtendedKeyUsageOID.SERVER_AUTH in usages
|
|
170
|
+
if not success:
|
|
171
|
+
logger.error(
|
|
172
|
+
f"The server certificate from bundle "
|
|
173
|
+
f"'{self._pemSimpleBank.bundleFilePath}' should contain "
|
|
174
|
+
f"'serverAuth' in extended key usage, got"
|
|
175
|
+
f"{usages}"
|
|
176
|
+
)
|
|
177
|
+
return success
|
|
178
|
+
|
|
179
|
+
if usageType == PEM_CHECK_TYPE.CHECK_TYPE_SSL_CLIENT_IDENTITY:
|
|
180
|
+
success = ExtendedKeyUsageOID.CLIENT_AUTH in usages
|
|
181
|
+
if not success:
|
|
182
|
+
logger.error(
|
|
183
|
+
f"The client certificate from bundle "
|
|
184
|
+
f"'{self._pemSimpleBank.bundleFilePath}' should contain "
|
|
185
|
+
f"'clientAuth' in extended key usage, got"
|
|
186
|
+
f"{usages}"
|
|
187
|
+
)
|
|
188
|
+
return success
|
|
189
|
+
|
|
190
|
+
logger.error(
|
|
191
|
+
f"invalid usageType '{usageType}' to check extended key usage with."
|
|
192
|
+
)
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
def _checkTrustChain(self, showCheck: bool) -> bool:
|
|
196
|
+
self._trustTree = self._buildTrustChain()
|
|
197
|
+
|
|
198
|
+
if len(self.trustChains) != 1:
|
|
199
|
+
logger.error(
|
|
200
|
+
f"1 and only 1 chain of trust is established, "
|
|
201
|
+
f"got {len(self.trustChains)} "
|
|
202
|
+
f"in '{self._pemSimpleBank.bundleFilePath}'"
|
|
203
|
+
)
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
# e.g. x509 certificate common names in order of
|
|
207
|
+
# root CA -> intermediate -> sub-intermediate -> cert
|
|
208
|
+
trustChainInCommonName = self.trustChains[0]
|
|
209
|
+
|
|
210
|
+
# drop root CA
|
|
211
|
+
trustChainInCommonName.pop(0)
|
|
212
|
+
# drop certificate
|
|
213
|
+
trustChainInCommonName.pop()
|
|
214
|
+
|
|
215
|
+
# sort intermediate CAs in bank
|
|
216
|
+
self._pemSimpleBank.intermediateCAs.clear()
|
|
217
|
+
for intermediateCACommonName in reversed(
|
|
218
|
+
trustChainInCommonName
|
|
219
|
+
): # bottom to top
|
|
220
|
+
self._pemSimpleBank.intermediateCAs.append(
|
|
221
|
+
self._trustTree.nodes[intermediateCACommonName].tag
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# verify the chain by openssl
|
|
225
|
+
if not self._verifyTrustChain():
|
|
226
|
+
logger.error(
|
|
227
|
+
f"PEM bundle failed trust chain check with openssl"
|
|
228
|
+
f"in file {self._pemSimpleBank.bundleFilePath}"
|
|
229
|
+
)
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
def _verifyTrustChain(self) -> bool:
|
|
235
|
+
numberCheckPass = self._checkNumberLimits(
|
|
236
|
+
maxCertificateCount=1,
|
|
237
|
+
maxPrivateKeyCount=1,
|
|
238
|
+
maxIntermediateCACount=self.MAX_COUNT,
|
|
239
|
+
maxRootCACount=1,
|
|
240
|
+
)
|
|
241
|
+
if not numberCheckPass:
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
store = X509Store()
|
|
245
|
+
|
|
246
|
+
rootCert = load_certificate(
|
|
247
|
+
FILETYPE_PEM, self._pemSimpleBank.rootCAs[0].as_bytes()
|
|
248
|
+
)
|
|
249
|
+
store.add_cert(rootCert)
|
|
250
|
+
|
|
251
|
+
for intermediateCA in self._pemSimpleBank.intermediateCAs:
|
|
252
|
+
intermediateCAObject = load_certificate(
|
|
253
|
+
FILETYPE_PEM, intermediateCA.as_bytes()
|
|
254
|
+
)
|
|
255
|
+
store.add_cert(intermediateCAObject)
|
|
256
|
+
|
|
257
|
+
untrustedCert = load_certificate(
|
|
258
|
+
FILETYPE_PEM, self._pemSimpleBank.certificates[0].as_bytes()
|
|
259
|
+
)
|
|
260
|
+
storeCtx = X509StoreContext(store, untrustedCert)
|
|
261
|
+
try:
|
|
262
|
+
storeCtx.verify_certificate()
|
|
263
|
+
return True
|
|
264
|
+
except X509StoreContextError:
|
|
265
|
+
logger.error(
|
|
266
|
+
f"PEM bundle failed to establish chain of trust in "
|
|
267
|
+
f"{self._pemSimpleBank.bundleFilePath}"
|
|
268
|
+
)
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
def _buildTrustChain(self) -> Tree:
|
|
272
|
+
trustTree = Tree()
|
|
273
|
+
|
|
274
|
+
numberCheckPass = self._checkNumberLimits(
|
|
275
|
+
maxCertificateCount=1,
|
|
276
|
+
maxPrivateKeyCount=1,
|
|
277
|
+
maxIntermediateCACount=self.MAX_COUNT,
|
|
278
|
+
maxRootCACount=1,
|
|
279
|
+
)
|
|
280
|
+
if not numberCheckPass:
|
|
281
|
+
return trustTree
|
|
282
|
+
|
|
283
|
+
# add root CA as root node
|
|
284
|
+
caCert: AbstractPEMObject = self._pemSimpleBank.rootCAs[0]
|
|
285
|
+
caCertParsed: Certificate = self._pemMap.get(caCert)
|
|
286
|
+
caCommonName = X509Util.getCommonName(caCertParsed)
|
|
287
|
+
trustTree.create_node(tag=caCert, identifier=caCommonName)
|
|
288
|
+
|
|
289
|
+
# add intermediate CAs to the root node
|
|
290
|
+
for intermediateCA in self._pemSimpleBank.intermediateCAs:
|
|
291
|
+
intermediateCA: AbstractPEMObject = intermediateCA
|
|
292
|
+
intermediateCAParsed: Certificate = self._pemMap.get(intermediateCA)
|
|
293
|
+
commonName = X509Util.getCommonName(intermediateCAParsed)
|
|
294
|
+
trustTree.create_node(
|
|
295
|
+
tag=intermediateCA,
|
|
296
|
+
identifier=commonName,
|
|
297
|
+
parent=caCommonName,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# add certificate to the root node
|
|
301
|
+
cert: AbstractPEMObject = self._pemSimpleBank.certificates[0]
|
|
302
|
+
certParsed: Certificate = self._pemMap.get(cert)
|
|
303
|
+
certificateCommonName = X509Util.getCommonName(certParsed)
|
|
304
|
+
trustTree.create_node(
|
|
305
|
+
tag=cert,
|
|
306
|
+
identifier=certificateCommonName,
|
|
307
|
+
parent=caCommonName,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# correct parent-child relations for all non-rootCA nodes
|
|
311
|
+
# 1) correct certificate
|
|
312
|
+
certificateIssuer = X509Util.getIssuer(certParsed)
|
|
313
|
+
trustTree.move_node(certificateCommonName, certificateIssuer)
|
|
314
|
+
# 2) correct intermediates
|
|
315
|
+
for intermediateCA in self._pemSimpleBank.intermediateCAs:
|
|
316
|
+
intermediateCAParsed: Certificate = self._pemMap.get(intermediateCA)
|
|
317
|
+
commonName = X509Util.getCommonName(intermediateCAParsed)
|
|
318
|
+
issuer = X509Util.getIssuer(intermediateCAParsed)
|
|
319
|
+
|
|
320
|
+
trustTree.move_node(commonName, issuer)
|
|
321
|
+
|
|
322
|
+
return trustTree
|
|
323
|
+
|
|
324
|
+
def _checkNumberLimits(
|
|
325
|
+
self,
|
|
326
|
+
maxCertificateCount,
|
|
327
|
+
maxIntermediateCACount,
|
|
328
|
+
maxPrivateKeyCount,
|
|
329
|
+
maxRootCACount,
|
|
330
|
+
) -> bool:
|
|
331
|
+
result = True
|
|
332
|
+
if len(self._pemSimpleBank.privateKeys) > maxPrivateKeyCount:
|
|
333
|
+
logger.error(
|
|
334
|
+
f"PEM bundle should contain no more than {maxPrivateKeyCount} "
|
|
335
|
+
f"private keys, "
|
|
336
|
+
f"got {len(self._pemSimpleBank.privateKeys)} "
|
|
337
|
+
f"in {self._pemSimpleBank.bundleFilePath}"
|
|
338
|
+
)
|
|
339
|
+
result = False
|
|
340
|
+
if len(self._pemSimpleBank.certificates) > maxCertificateCount:
|
|
341
|
+
logger.error(
|
|
342
|
+
f"PEM bundle should contain no more than {maxCertificateCount} "
|
|
343
|
+
f"certificates, "
|
|
344
|
+
f"got {len(self._pemSimpleBank.certificates)} "
|
|
345
|
+
f"in {self._pemSimpleBank.bundleFilePath}"
|
|
346
|
+
)
|
|
347
|
+
result = False
|
|
348
|
+
if len(self._pemSimpleBank.intermediateCAs) > maxIntermediateCACount:
|
|
349
|
+
logger.error(
|
|
350
|
+
f"PEM bundle should contain no more than "
|
|
351
|
+
f"{maxIntermediateCACount} intermediate CAs, "
|
|
352
|
+
f"got {len(self._pemSimpleBank.intermediateCAs)} "
|
|
353
|
+
f"in {self._pemSimpleBank.bundleFilePath}"
|
|
354
|
+
)
|
|
355
|
+
result = False
|
|
356
|
+
if len(self._pemSimpleBank.rootCAs) > maxRootCACount:
|
|
357
|
+
logger.error(
|
|
358
|
+
f"PEM bundle should contain no more than {maxRootCACount} "
|
|
359
|
+
f"root CAs "
|
|
360
|
+
f"got {len(self._pemSimpleBank.rootCAs)} "
|
|
361
|
+
f"in {self._pemSimpleBank.bundleFilePath}"
|
|
362
|
+
)
|
|
363
|
+
result = False
|
|
364
|
+
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class X509Util:
|
|
369
|
+
@staticmethod
|
|
370
|
+
def getCommonName(x509: Certificate) -> str:
|
|
371
|
+
return x509.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def getSubject(x509: Certificate) -> str:
|
|
375
|
+
return str(x509.subject)
|
|
376
|
+
|
|
377
|
+
@staticmethod
|
|
378
|
+
def getIssuer(x509: Certificate) -> str:
|
|
379
|
+
return x509.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
def getIsCA(x509: Certificate) -> bool:
|
|
383
|
+
try:
|
|
384
|
+
basicConstraintExtension: Extension = (
|
|
385
|
+
x509.extensions.get_extension_for_class(BasicConstraints)
|
|
386
|
+
)
|
|
387
|
+
return basicConstraintExtension.value.ca
|
|
388
|
+
|
|
389
|
+
except ExtensionNotFound:
|
|
390
|
+
return False
|
|
391
|
+
|
|
392
|
+
@staticmethod
|
|
393
|
+
def getNotValidBefore(x509: Certificate) -> datetime:
|
|
394
|
+
return x509.not_valid_before
|
|
395
|
+
|
|
396
|
+
@staticmethod
|
|
397
|
+
def getNotValidAfter(x509: Certificate) -> datetime:
|
|
398
|
+
return x509.not_valid_after
|
|
399
|
+
|
|
400
|
+
@staticmethod
|
|
401
|
+
def getExtendedKeyUsages(x509: Certificate) -> List:
|
|
402
|
+
try:
|
|
403
|
+
extension: Extension = x509.extensions.get_extension_for_class(
|
|
404
|
+
ExtendedKeyUsage
|
|
405
|
+
)
|
|
406
|
+
return extension.value._usages
|
|
407
|
+
|
|
408
|
+
except ExtensionNotFound:
|
|
409
|
+
return []
|
|
410
|
+
|
|
411
|
+
@staticmethod
|
|
412
|
+
def getX509Version(x509: Certificate) -> str:
|
|
413
|
+
return x509.version.name
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class PemBundleError(Exception):
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class PemBundleParser:
|
|
421
|
+
def __init__(self):
|
|
422
|
+
self._pemMap = {}
|
|
423
|
+
self._pem = None
|
|
424
|
+
|
|
425
|
+
self._privateKeys: List[AbstractPEMObject] = []
|
|
426
|
+
self._certificates: List[AbstractPEMObject] = []
|
|
427
|
+
self._rootCAs: List[AbstractPEMObject] = []
|
|
428
|
+
self._intermediateCAs: List[AbstractPEMObject] = []
|
|
429
|
+
|
|
430
|
+
def parse(self, pemFilePath: str) -> PemBank:
|
|
431
|
+
self._loadPemBundleFile(pemFilePath)
|
|
432
|
+
|
|
433
|
+
# simple classification for PemSimpleBank
|
|
434
|
+
inspectedPems = set([])
|
|
435
|
+
for pemSection, parsedPem in self._pemMap.items():
|
|
436
|
+
# if private key
|
|
437
|
+
if hasattr(parsedPem, "private_bytes"):
|
|
438
|
+
self._privateKeys.append(pemSection)
|
|
439
|
+
inspectedPems.add(pemSection)
|
|
440
|
+
|
|
441
|
+
# if x509 certificate
|
|
442
|
+
if isinstance(parsedPem, Certificate):
|
|
443
|
+
commonName = X509Util.getCommonName(parsedPem)
|
|
444
|
+
issuer = X509Util.getIssuer(parsedPem)
|
|
445
|
+
isCA = X509Util.getIsCA(parsedPem)
|
|
446
|
+
|
|
447
|
+
if isCA:
|
|
448
|
+
if commonName == issuer:
|
|
449
|
+
# found root CA
|
|
450
|
+
self._rootCAs.append(pemSection)
|
|
451
|
+
inspectedPems.add(pemSection)
|
|
452
|
+
else:
|
|
453
|
+
# found intermediate CA
|
|
454
|
+
self._intermediateCAs.append(pemSection)
|
|
455
|
+
inspectedPems.add(pemSection)
|
|
456
|
+
else:
|
|
457
|
+
self._certificates.append(pemSection)
|
|
458
|
+
inspectedPems.add(pemSection)
|
|
459
|
+
|
|
460
|
+
pemSimpleBank = PemSimpleBank(
|
|
461
|
+
privateKeys=self._privateKeys,
|
|
462
|
+
certificates=self._certificates,
|
|
463
|
+
rootCAs=self._rootCAs,
|
|
464
|
+
intermediateCAs=self._intermediateCAs,
|
|
465
|
+
bundleFilePath=pemFilePath,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
return PemBank(pemSimpleBank, self._pemMap)
|
|
469
|
+
|
|
470
|
+
def _loadPemBundleFile(self, pemFilePath):
|
|
471
|
+
try:
|
|
472
|
+
pemFile = Path(pemFilePath)
|
|
473
|
+
if not pemFile.is_file() or not pemFile.exists():
|
|
474
|
+
logger.error("Pem bundle '%s' is not found", pemFilePath)
|
|
475
|
+
except Exception:
|
|
476
|
+
logger.error("Pem bundle '%s' is not found", pemFilePath)
|
|
477
|
+
|
|
478
|
+
self._pem = pem.parse_file(pemFilePath)
|
|
479
|
+
for p in self._pem:
|
|
480
|
+
# supported types:
|
|
481
|
+
# x509 certificates
|
|
482
|
+
# private keys
|
|
483
|
+
for loadMethod in [
|
|
484
|
+
load_pem_x509_certificate,
|
|
485
|
+
partial(load_pem_private_key, password=None),
|
|
486
|
+
]:
|
|
487
|
+
try:
|
|
488
|
+
parsed = loadMethod(p.as_bytes())
|
|
489
|
+
self._pemMap[p] = parsed
|
|
490
|
+
except ValueError:
|
|
491
|
+
# on parse error from loadMethod
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
# check if every PEM section is loaded
|
|
495
|
+
if len(self._pemMap) != len(self._pem):
|
|
496
|
+
unknownSections = set(self._pem) - self._pemMap.keys()
|
|
497
|
+
unknownSectionHeads = [
|
|
498
|
+
f"{s.as_text()[: min(50, len(s.as_text()))]}..."
|
|
499
|
+
for s in unknownSections
|
|
500
|
+
]
|
|
501
|
+
logger.exception(
|
|
502
|
+
f"unknown PEM section(s) in file {pemFilePath}: "
|
|
503
|
+
f"{unknownSectionHeads}"
|
|
504
|
+
)
|
|
505
|
+
raise
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
PemInfo = namedtuple(
|
|
509
|
+
"PemInfo",
|
|
510
|
+
["pemType", "pemStart", "pemEnding", "summary", "pemFor"],
|
|
511
|
+
defaults=(None, None, None, {}, None),
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
class PemViewer:
|
|
516
|
+
PEM_TYPE_PRIVATE_KEY = "private key"
|
|
517
|
+
PEM_TYPE_CERTIFICATE = "certificate"
|
|
518
|
+
PEM_TYPE_DH_PARAMETER = "Diffie-Hellman parameter"
|
|
519
|
+
|
|
520
|
+
FOR_PRIVATE_KEY = "private key"
|
|
521
|
+
FOR_SERVER_CERTIFICATE = "server certificate"
|
|
522
|
+
FOR_CLIENT_CERTIFICATE = "client certificate"
|
|
523
|
+
FOR_INTERMEDIATE_CA = "intermediate CA"
|
|
524
|
+
FOR_ROOT_CA = "root CA"
|
|
525
|
+
FOR_MTLS_CA = "mTLS CA"
|
|
526
|
+
FOR_MTLS_PEER = "mTLS peer certificate"
|
|
527
|
+
FOR_DH_PARAMETER = "Diffie-Hellman parameter"
|
|
528
|
+
|
|
529
|
+
def __init__(self, pemBundleFilePath: str):
|
|
530
|
+
self._pemBundleFilePath: str = pemBundleFilePath
|
|
531
|
+
self._inspections: Dict[bytes, PemInfo] = {}
|
|
532
|
+
|
|
533
|
+
def log(self, pemBundleName: str):
|
|
534
|
+
defaultLogLevel = logging.root.level
|
|
535
|
+
messageHeader = f"{pemBundleName} PEM bundle contains: \n"
|
|
536
|
+
messages = []
|
|
537
|
+
for pemInfo in self._inspections.values():
|
|
538
|
+
summary = [
|
|
539
|
+
textwrap.indent(f"{key}: '{value}'", " ")
|
|
540
|
+
for key, value in pemInfo.summary.items()
|
|
541
|
+
]
|
|
542
|
+
|
|
543
|
+
summaryLine = "\n".join(summary)
|
|
544
|
+
if defaultLogLevel < logging.INFO:
|
|
545
|
+
messages.append(
|
|
546
|
+
(
|
|
547
|
+
f" A {pemInfo.pemType} for {pemInfo.pemFor.upper()} with\n"
|
|
548
|
+
f"{summaryLine}\n"
|
|
549
|
+
f" from PEM section "
|
|
550
|
+
f"'{pemInfo.pemStart} ... {pemInfo.pemEnding}'"
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
else:
|
|
554
|
+
message = f"a {pemInfo.pemType} for {pemInfo.pemFor.upper()}"
|
|
555
|
+
if pemInfo.pemType == self.PEM_TYPE_CERTIFICATE:
|
|
556
|
+
message += f" with subject {pemInfo.summary['subject']}"
|
|
557
|
+
messages.append(message)
|
|
558
|
+
|
|
559
|
+
if defaultLogLevel < logging.INFO:
|
|
560
|
+
seperator = "\n"
|
|
561
|
+
else:
|
|
562
|
+
seperator = ", "
|
|
563
|
+
|
|
564
|
+
outMsg = messageHeader + seperator.join(messages)
|
|
565
|
+
|
|
566
|
+
for index, line in enumerate(outMsg.splitlines()):
|
|
567
|
+
logger.log(defaultLogLevel, (" " if index else "") + line)
|
|
568
|
+
|
|
569
|
+
def _getPemStart(self, pemBytes: bytes) -> str:
|
|
570
|
+
return pemBytes[: min(50, len(pemBytes))].decode().replace("\n", "")
|
|
571
|
+
|
|
572
|
+
def _getPemEnding(self, pemBytes: bytes) -> str:
|
|
573
|
+
return pemBytes[-min(50, len(pemBytes)) :].decode().replace("\n", "")
|
|
574
|
+
|
|
575
|
+
def viewPrivateKey(self, pemBytes: bytes, pemFor: str = None):
|
|
576
|
+
key = load_pem_private_key(pemBytes, password=None)
|
|
577
|
+
pemInfo = PemInfo(
|
|
578
|
+
pemFor=pemFor,
|
|
579
|
+
pemType=self.PEM_TYPE_PRIVATE_KEY,
|
|
580
|
+
pemStart=self._getPemStart(pemBytes),
|
|
581
|
+
pemEnding=self._getPemEnding(pemBytes),
|
|
582
|
+
summary={"publicKey": key.public_key()},
|
|
583
|
+
)
|
|
584
|
+
self._inspections[pemBytes] = pemInfo
|
|
585
|
+
|
|
586
|
+
def viewCertificate(self, pemBytes: bytes, pemFor: str = None):
|
|
587
|
+
cert = load_pem_x509_certificate(pemBytes)
|
|
588
|
+
pemInfo = PemInfo(
|
|
589
|
+
pemFor=pemFor,
|
|
590
|
+
pemType=self.PEM_TYPE_CERTIFICATE,
|
|
591
|
+
pemStart=self._getPemStart(pemBytes),
|
|
592
|
+
pemEnding=self._getPemEnding(pemBytes),
|
|
593
|
+
summary={
|
|
594
|
+
"subject": X509Util.getSubject(cert),
|
|
595
|
+
"issuer": X509Util.getIssuer(cert),
|
|
596
|
+
"notValidBefore": X509Util.getNotValidBefore(cert),
|
|
597
|
+
"notValidAfter": X509Util.getNotValidAfter(cert),
|
|
598
|
+
"isCa": X509Util.getIsCA(cert),
|
|
599
|
+
"x509Version": X509Util.getX509Version(cert),
|
|
600
|
+
"extendedKeyUsage": X509Util.getExtendedKeyUsages(cert),
|
|
601
|
+
},
|
|
602
|
+
)
|
|
603
|
+
self._inspections[pemBytes] = pemInfo
|
|
604
|
+
|
|
605
|
+
def viewDiffieHellmanParameter(self, pemBytes: bytes, pemFor: str = None):
|
|
606
|
+
dhParam = load_pem_parameters(pemBytes)
|
|
607
|
+
pemInfo = PemInfo(
|
|
608
|
+
pemFor=pemFor,
|
|
609
|
+
pemType=self.PEM_TYPE_DH_PARAMETER,
|
|
610
|
+
pemStart=self._getPemStart(pemBytes),
|
|
611
|
+
pemEnding=self._getPemEnding(pemBytes),
|
|
612
|
+
summary={"length": dhParam.generate_private_key().key_size},
|
|
613
|
+
)
|
|
614
|
+
self._inspections[pemBytes] = pemInfo
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def parseTrustRootFromBundle(pemFilePath: str) -> List[TxCertificate]:
|
|
618
|
+
# parse PEM format
|
|
619
|
+
parser = PemBundleParser()
|
|
620
|
+
bank = parser.parse(pemFilePath)
|
|
621
|
+
bank.check(PEM_CHECK_TYPE.CHECK_TYPE_SSL_MTLS_CA)
|
|
622
|
+
|
|
623
|
+
pemInspector = PemViewer(pemFilePath)
|
|
624
|
+
# load certificates in the PEM bundle file
|
|
625
|
+
trustedPeerAuthorities = []
|
|
626
|
+
for trustedPeerCertificateAuthorityPEM in chain(
|
|
627
|
+
bank.intermediateCAs, bank.rootCAs
|
|
628
|
+
):
|
|
629
|
+
pemBytes = trustedPeerCertificateAuthorityPEM.as_bytes()
|
|
630
|
+
trustedPeerAuthorities.append(ssl.Certificate.loadPEM(pemBytes))
|
|
631
|
+
pemInspector.viewCertificate(pemBytes, pemFor=pemInspector.FOR_MTLS_CA)
|
|
632
|
+
pemInspector.log(pemBundleName="mTLS trusted CA")
|
|
633
|
+
|
|
634
|
+
return trustedPeerAuthorities
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
PrivateKeyWithFullChain = namedtuple(
|
|
638
|
+
"PrivateKeyWithFullChain",
|
|
639
|
+
["privateKey", "certificate", "intermediateCAs", "rootCA"],
|
|
640
|
+
defaults=[None, None, [], None],
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def parsePemBundleForServer(pemFilePath: str) -> PrivateKeyWithFullChain:
|
|
645
|
+
parser = PemBundleParser()
|
|
646
|
+
bank = parser.parse(pemFilePath)
|
|
647
|
+
bank.check(PEM_CHECK_TYPE.CHECK_TYPE_SSL_SERVER_IDENTITY)
|
|
648
|
+
|
|
649
|
+
pemViewer = PemViewer(pemFilePath)
|
|
650
|
+
|
|
651
|
+
key = bank.privateKeys[0]
|
|
652
|
+
pemViewer.viewPrivateKey(key.as_bytes(), pemFor=pemViewer.FOR_PRIVATE_KEY)
|
|
653
|
+
|
|
654
|
+
cert = bank.certificates[0]
|
|
655
|
+
pemViewer.viewCertificate(
|
|
656
|
+
cert.as_bytes(), pemFor=pemViewer.FOR_SERVER_CERTIFICATE
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
intermediates = bank.intermediateCAs
|
|
660
|
+
for intermediate in intermediates:
|
|
661
|
+
pemViewer.viewCertificate(
|
|
662
|
+
intermediate.as_bytes(), pemFor=pemViewer.FOR_INTERMEDIATE_CA
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
root = bank.rootCAs[0]
|
|
666
|
+
pemViewer.viewCertificate(root.as_bytes(), pemFor=pemViewer.FOR_ROOT_CA)
|
|
667
|
+
|
|
668
|
+
pemViewer.log(pemBundleName="TLS Server")
|
|
669
|
+
|
|
670
|
+
return PrivateKeyWithFullChain(
|
|
671
|
+
privateKey=key,
|
|
672
|
+
certificate=cert,
|
|
673
|
+
intermediateCAs=intermediates,
|
|
674
|
+
rootCA=root,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def parsePemBundleForClient(pemFilePath: str) -> PrivateKeyWithFullChain:
|
|
679
|
+
parser = PemBundleParser()
|
|
680
|
+
bank = parser.parse(pemFilePath)
|
|
681
|
+
bank.check(PEM_CHECK_TYPE.CHECK_TYPE_SSL_CLIENT_IDENTITY)
|
|
682
|
+
|
|
683
|
+
pemViewer = PemViewer(pemFilePath)
|
|
684
|
+
|
|
685
|
+
key = bank.privateKeys[0]
|
|
686
|
+
pemViewer.viewPrivateKey(key.as_bytes(), pemFor=pemViewer.FOR_PRIVATE_KEY)
|
|
687
|
+
|
|
688
|
+
cert = bank.certificates[0]
|
|
689
|
+
pemViewer.viewCertificate(
|
|
690
|
+
cert.as_bytes(), pemFor=pemViewer.FOR_CLIENT_CERTIFICATE
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
intermediates = bank.intermediateCAs
|
|
694
|
+
for intermediate in intermediates:
|
|
695
|
+
pemViewer.viewCertificate(
|
|
696
|
+
intermediate.as_bytes(), pemFor=pemViewer.FOR_INTERMEDIATE_CA
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
root = bank.rootCAs[0]
|
|
700
|
+
pemViewer.viewCertificate(root.as_bytes(), pemFor=pemViewer.FOR_ROOT_CA)
|
|
701
|
+
|
|
702
|
+
pemViewer.log(pemBundleName="TLS Client")
|
|
703
|
+
|
|
704
|
+
return PrivateKeyWithFullChain(
|
|
705
|
+
privateKey=key,
|
|
706
|
+
certificate=cert,
|
|
707
|
+
intermediateCAs=intermediates,
|
|
708
|
+
rootCA=root,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def generateDiffieHellmanParameterBytes(
|
|
713
|
+
outputFilePath: str, length=2048
|
|
714
|
+
) -> bytes:
|
|
715
|
+
p = dh.generate_parameters(2, length)
|
|
716
|
+
dhBytes = p.parameter_bytes(Encoding.PEM, ParameterFormat.PKCS3)
|
|
717
|
+
with open(outputFilePath, "wb") as outputPem:
|
|
718
|
+
outputPem.write(dhBytes)
|
|
719
|
+
return dhBytes
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def parseDiffieHellmanParameter(
|
|
723
|
+
pemFilePath: str,
|
|
724
|
+
) -> OpenSSLDiffieHellmanParameters:
|
|
725
|
+
file = FilePath(pemFilePath)
|
|
726
|
+
pemViewer = PemViewer(pemFilePath)
|
|
727
|
+
pemViewer.viewDiffieHellmanParameter(
|
|
728
|
+
file.getContent(), pemFor=pemViewer.FOR_DH_PARAMETER
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
pemViewer.log(pemBundleName="Diffie-Hellman Parameter")
|
|
732
|
+
return ssl.DiffieHellmanParameters.fromFile(file)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def parsePemBundleForTrustedPeers(pemFilePath: str) -> List[Certificate]:
|
|
736
|
+
parser = PemBundleParser()
|
|
737
|
+
bank = parser.parse(pemFilePath)
|
|
738
|
+
bank.check(PEM_CHECK_TYPE.CHECK_TYPE_SSL_MTLS_PEER)
|
|
739
|
+
|
|
740
|
+
trustedPeerCertificates = bank.certificates
|
|
741
|
+
|
|
742
|
+
pemViewer = PemViewer(pemFilePath)
|
|
743
|
+
for trustedPeerCertificate in trustedPeerCertificates:
|
|
744
|
+
pemViewer.viewCertificate(
|
|
745
|
+
trustedPeerCertificate.as_bytes(), pemFor=pemViewer.FOR_MTLS_PEER
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
pemViewer.log(pemBundleName="mTLS trusted peers")
|
|
749
|
+
|
|
750
|
+
return trustedPeerCertificates
|