cert-utils 1.2.0__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.
Files changed (79) hide show
  1. cert_utils/__init__.py +8 -0
  2. cert_utils/compat/__init__.py +0 -0
  3. cert_utils/compat/cryptography.py +172 -0
  4. cert_utils/compat/openssl.py +860 -0
  5. cert_utils/compat/pyopenssl.py +207 -0
  6. cert_utils/compat/signatures.py +114 -0
  7. cert_utils/conditionals.py +121 -0
  8. cert_utils/convert.py +307 -0
  9. cert_utils/core.py +3075 -0
  10. cert_utils/errors.py +54 -0
  11. cert_utils/letsencrypt-certs/deprecated/e5-cross.pem +26 -0
  12. cert_utils/letsencrypt-certs/deprecated/e5.pem +17 -0
  13. cert_utils/letsencrypt-certs/deprecated/e6-cross.pem +26 -0
  14. cert_utils/letsencrypt-certs/deprecated/e6.pem +17 -0
  15. cert_utils/letsencrypt-certs/deprecated/e7-cross.pem +26 -0
  16. cert_utils/letsencrypt-certs/deprecated/e7.pem +17 -0
  17. cert_utils/letsencrypt-certs/deprecated/e8-cross.pem +26 -0
  18. cert_utils/letsencrypt-certs/deprecated/e8.pem +17 -0
  19. cert_utils/letsencrypt-certs/deprecated/e9-cross.pem +26 -0
  20. cert_utils/letsencrypt-certs/deprecated/e9.pem +17 -0
  21. cert_utils/letsencrypt-certs/deprecated/fakeleintermediatex1.pem +28 -0
  22. cert_utils/letsencrypt-certs/deprecated/fakelerootx1.pem +29 -0
  23. cert_utils/letsencrypt-certs/deprecated/isrg-root-ocsp-x1.pem +28 -0
  24. cert_utils/letsencrypt-certs/deprecated/isrg-root-x1-cross-signed.pem +31 -0
  25. cert_utils/letsencrypt-certs/deprecated/isrg-root-x2-cross-signed.pem +26 -0
  26. cert_utils/letsencrypt-certs/deprecated/lets-encrypt-e1.pem +17 -0
  27. cert_utils/letsencrypt-certs/deprecated/lets-encrypt-e2.pem +17 -0
  28. cert_utils/letsencrypt-certs/deprecated/lets-encrypt-r3-cross-signed.pem +26 -0
  29. cert_utils/letsencrypt-certs/deprecated/lets-encrypt-r3.pem +30 -0
  30. cert_utils/letsencrypt-certs/deprecated/lets-encrypt-r4-cross-signed.pem +26 -0
  31. cert_utils/letsencrypt-certs/deprecated/lets-encrypt-r4.pem +30 -0
  32. cert_utils/letsencrypt-certs/deprecated/lets-encrypt-x1-cross-signed.pem +27 -0
  33. cert_utils/letsencrypt-certs/deprecated/lets-encrypt-x2-cross-signed.pem +27 -0
  34. cert_utils/letsencrypt-certs/deprecated/lets-encrypt-x3-cross-signed.pem +27 -0
  35. cert_utils/letsencrypt-certs/deprecated/lets-encrypt-x4-cross-signed.pem +27 -0
  36. cert_utils/letsencrypt-certs/deprecated/letsencrypt-stg-root-x2-signed-by-x1.pem +27 -0
  37. cert_utils/letsencrypt-certs/deprecated/letsencryptauthorityx1.pem +32 -0
  38. cert_utils/letsencrypt-certs/deprecated/letsencryptauthorityx2.pem +32 -0
  39. cert_utils/letsencrypt-certs/deprecated/letsencryptauthorityx3.pem +32 -0
  40. cert_utils/letsencrypt-certs/deprecated/letsencryptauthorityx4.pem +32 -0
  41. cert_utils/letsencrypt-certs/deprecated/r10.pem +29 -0
  42. cert_utils/letsencrypt-certs/deprecated/r11.pem +29 -0
  43. cert_utils/letsencrypt-certs/deprecated/r12.pem +29 -0
  44. cert_utils/letsencrypt-certs/deprecated/r13.pem +29 -0
  45. cert_utils/letsencrypt-certs/deprecated/r14.pem +29 -0
  46. cert_utils/letsencrypt-certs/deprecated/trustid-x3-root.p7c +0 -0
  47. cert_utils/letsencrypt-certs/deprecated/trustid-x3-root.pem +20 -0
  48. cert_utils/letsencrypt-certs/int-ye1.pem +16 -0
  49. cert_utils/letsencrypt-certs/int-ye2.pem +16 -0
  50. cert_utils/letsencrypt-certs/int-ye3.pem +16 -0
  51. cert_utils/letsencrypt-certs/int-yr1.pem +28 -0
  52. cert_utils/letsencrypt-certs/int-yr2.pem +28 -0
  53. cert_utils/letsencrypt-certs/int-yr3.pem +28 -0
  54. cert_utils/letsencrypt-certs/isrg-root-x2.pem +14 -0
  55. cert_utils/letsencrypt-certs/isrgrootx1.pem +31 -0
  56. cert_utils/letsencrypt-certs/isrgrootx1.pkix +0 -0
  57. cert_utils/letsencrypt-certs/letsencrypt-stg-root-x1.pem +32 -0
  58. cert_utils/letsencrypt-certs/letsencrypt-stg-root-x2.pem +15 -0
  59. cert_utils/letsencrypt-certs/root-x2-by-x1.pem +26 -0
  60. cert_utils/letsencrypt-certs/root-ye-by-x2.pem +17 -0
  61. cert_utils/letsencrypt-certs/root-ye.pem +12 -0
  62. cert_utils/letsencrypt-certs/root-yr-by-x1.pem +34 -0
  63. cert_utils/letsencrypt-certs/root-yr.pem +30 -0
  64. cert_utils/letsencrypt_info/__init__.py +6 -0
  65. cert_utils/letsencrypt_info/certificates.py +1074 -0
  66. cert_utils/letsencrypt_info/rate_limits.py +414 -0
  67. cert_utils/letsencrypt_info/types.py +120 -0
  68. cert_utils/model.py +181 -0
  69. cert_utils/network.py +28 -0
  70. cert_utils/py.typed +0 -0
  71. cert_utils/scripts/__init__.py +0 -0
  72. cert_utils/scripts/cert_info.py +332 -0
  73. cert_utils/utils.py +388 -0
  74. cert_utils-1.2.0.dist-info/METADATA +259 -0
  75. cert_utils-1.2.0.dist-info/RECORD +79 -0
  76. cert_utils-1.2.0.dist-info/WHEEL +5 -0
  77. cert_utils-1.2.0.dist-info/entry_points.txt +2 -0
  78. cert_utils-1.2.0.dist-info/licenses/LICENSE.txt +224 -0
  79. cert_utils-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,860 @@
1
+ # stdlib
2
+ import binascii
3
+ import re
4
+ import subprocess
5
+ from typing import List
6
+ from typing import Optional
7
+ from typing import Tuple
8
+ from typing import Union
9
+
10
+ # pypi
11
+ import psutil
12
+
13
+ # local
14
+ from .. import core
15
+ from ..errors import FallbackError_FilepathRequired
16
+ from ..errors import OpenSslError
17
+ from ..errors import OpenSslError_InvalidCertificate
18
+ from ..errors import OpenSslError_InvalidCSR
19
+ from ..errors import OpenSslError_InvalidKey
20
+ from ..errors import OpenSslError_VersionTooLow
21
+ from ..utils import new_pem_tempfile
22
+ from ..utils import TECHNOLOGY_RETURN_VALUES
23
+
24
+ # ==============================================================================
25
+
26
+
27
+ # note the conditional whitespace before/after `CN`
28
+ # this is because of differing openssl versions
29
+ RE_openssl_x509_subject = re.compile(r"Subject:.*? CN ?= ?([^\s,;/]+)")
30
+ RE_openssl_x509_san = re.compile(
31
+ r"X509v3 Subject Alternative Name: ?\n +([^\n]+)\n?", re.MULTILINE | re.DOTALL
32
+ )
33
+
34
+
35
+ # openssl 3 does not have "keyid:" as a prefix
36
+ # a keyid prefix is okay!
37
+ # we do not want the alternates, which are uri+serial; but take that out in results
38
+ RE_openssl_x509_authority_key_identifier = re.compile(
39
+ r"X509v3 Authority Key Identifier: ?\n +(?:keyid:)?([^\n]+)\n?",
40
+ re.MULTILINE | re.DOTALL,
41
+ )
42
+ # we have a potential line in there for the OSCP or something else.
43
+ RE_openssl_x509_issuer_uri = re.compile(
44
+ r"Authority Information Access: ?\n(?:[^\n]*^\n)? +CA Issuers - URI:([^\n]+)\n?",
45
+ re.MULTILINE | re.DOTALL,
46
+ )
47
+
48
+ RE_openssl_x509_serial = re.compile(r"Serial Number: ?(\d+)")
49
+
50
+
51
+ # depending on openssl version, the "Public key: " text might list the bits
52
+ # it may or may not also have a dash in the phrase "Public Key"
53
+ # it may or may not be prefaced with the PublicKey type
54
+ RE_openssl_x509_keytype_rsa = re.compile(
55
+ r"Subject Public Key Info:\n"
56
+ r"\s+Public Key Algorithm: rsaEncryption\n"
57
+ r"\s+(?:RSA )?Public[\-\ ]?Key: \((\d+) bits?\)",
58
+ re.MULTILINE,
59
+ )
60
+ RE_openssl_x509_keytype_ec = re.compile(
61
+ r"Subject Public Key Info:\n"
62
+ r"\s+Public Key Algorithm: id-ecPublicKey\n"
63
+ r"\s+(?:EC )?Public[\-\ ]?Key: \((\d+) bits?\)\n"
64
+ r"\s+pub:\n"
65
+ r"[\s\d\w\:]+"
66
+ r"(?:"
67
+ r"(?:\s+ASN1 OID: ([\w]+)\n)"
68
+ r"|"
69
+ r"(?:\s+NIST CURVE: ([\-\w]+)\n)"
70
+ r")",
71
+ re.MULTILINE,
72
+ )
73
+
74
+
75
+ def _cleanup_openssl_modulus(data: str) -> str:
76
+ data = data.strip()
77
+ if data[:8] == "Modulus=":
78
+ data = data[8:]
79
+ return data
80
+
81
+
82
+ def _format_openssl_components(
83
+ data: str,
84
+ fieldset: Optional[str] = None,
85
+ ) -> str:
86
+ """
87
+ different openssl versions give different responses. FUN.
88
+
89
+ To make things easier, just format this into the crypto compatible payload,
90
+ then invoke the crypto formattter
91
+
92
+ openssl = [0, 9, 8]
93
+ subject= /C=US/O=Internet Security Research Group/CN=ISRG Root X2
94
+
95
+ openssl = [1, 1, 1]
96
+ issuer=C = US, O = Internet Security Research Group, CN = ISRG Root X2
97
+ """
98
+ # print(core.openssl_version, data)
99
+ if fieldset in ("issuer", "subject"):
100
+ if fieldset == "issuer":
101
+ if data.startswith("issuer= "):
102
+ data = data[8:]
103
+ elif data.startswith("issuer="):
104
+ data = data[7:]
105
+ elif fieldset == "subject":
106
+ if data.startswith("subject= "):
107
+ data = data[9:]
108
+ elif data.startswith("subject="):
109
+ data = data[8:]
110
+ data_list: List[str]
111
+ if "/" in data:
112
+ data_list = [i.strip() for i in data.split("/")]
113
+ elif "," in data:
114
+ data_list = [i.strip() for i in data.split(",")]
115
+ else:
116
+ data_list = [
117
+ data,
118
+ ]
119
+ _out = []
120
+ for _cset in data_list:
121
+ _cset_split = _cset.split("=")
122
+ _cset_edited = tuple(i.strip() for i in _cset_split)
123
+ _out.append(_cset_edited)
124
+ return _format_crypto_components(_out, fieldset=fieldset)
125
+ else:
126
+ raise ValueError("invalid fieldset")
127
+
128
+
129
+ def _format_crypto_components(
130
+ data: Union[
131
+ List[str],
132
+ List[Tuple[str, ...]],
133
+ List[Tuple[bytes, bytes]],
134
+ ],
135
+ fieldset: Optional[str] = None,
136
+ ) -> str:
137
+ """
138
+ :param data: input
139
+ :param fieldset: is unused. would be "issuer" or "subject"
140
+
141
+ `get_components()` is somewhat structured
142
+ the following are valid:
143
+ * [('CN', 'Pebble Intermediate CA 601ea1')]
144
+ * [('C', 'US'), ('O', 'Internet Security Research Group'), ('CN', 'ISRG Root X2')]
145
+ * [('C', 'US'), ('O', 'Internet Security Research Group'), ('CN', 'ISRG Root X1')]
146
+ * [('O', 'Digital Signature Trust Co.'), ('CN', 'DST Root CA X3')]
147
+ cert = openssl_crypto.load_certificate(openssl_crypto.FILETYPE_PEM, cert_pem)
148
+ _issuer = cert.get_issuer().get_components()
149
+ _subject = cert.get_subject().get_components()
150
+ """
151
+ _out = []
152
+ for _in_set in data:
153
+ _converted = [i.decode("utf8") if isinstance(i, bytes) else i for i in _in_set] # type: ignore[attr-defined]
154
+ _out.append("=".join(_converted))
155
+ out = "\n".join(_out).strip()
156
+ return out
157
+
158
+
159
+ def _openssl_cert__normalize_pem(cert_pem: str) -> str:
160
+ """
161
+ normalize a cert using openssl
162
+ NOTE: this is an openssl fallback routine
163
+
164
+ :param cert_pem: PEM encoded Certificate data
165
+ :type cert_pem: str
166
+ :returns: normalized Certificate
167
+ :rtype: str
168
+
169
+ This runs via OpenSSL:
170
+
171
+ openssl x509 -in {FILEPATH}
172
+ """
173
+ if core.openssl_version is None:
174
+ core.check_openssl_version()
175
+
176
+ _tmpfile_pem = new_pem_tempfile(cert_pem)
177
+ try:
178
+ cert_pem_filepath = _tmpfile_pem.name
179
+ with psutil.Popen(
180
+ [core.openssl_path, "x509", "-in", cert_pem_filepath],
181
+ stdout=subprocess.PIPE,
182
+ stderr=subprocess.PIPE,
183
+ ) as proc:
184
+ data_bytes, err = proc.communicate()
185
+ if not data_bytes:
186
+ raise OpenSslError_InvalidCertificate(err)
187
+ data_str = data_bytes.decode("utf8")
188
+ data_str = data_str.strip()
189
+ return data_str
190
+ except Exception as exc: # noqa: F841
191
+ raise
192
+ finally:
193
+ _tmpfile_pem.close()
194
+
195
+
196
+ def _openssl_spki_hash_cert(
197
+ key_technology_basic: str = "",
198
+ cert_pem_filepath: str = "",
199
+ as_b64: Optional[bool] = None,
200
+ ) -> str:
201
+ """
202
+ :param key_technology_basic: Is the key an "EC" or "RSA" key?
203
+ :type key_technology_basic: str
204
+ :param cert_pem_filepath: REQUIRED filepath to PEM Certificate.
205
+ Used for commandline OpenSSL fallback operations.
206
+ :type cert_pem_filepath: str
207
+ :param as_b64: Should the result be returned in Base64 encoding? default None
208
+ :type as_b64: boolean
209
+ :returns: spki_hash
210
+ :rtype: str
211
+
212
+ The OpenSSL Equivalent / Fallback is::
213
+
214
+ openssl x509 -pubkey -noout -in {CERT_FILEPATH} | \
215
+ openssl {key_technology_basic} -pubout -outform DER -pubin | \
216
+ openssl dgst -sha256 -binary | \
217
+ openssl enc -base64
218
+ """
219
+ if key_technology_basic not in ("EC", "RSA"):
220
+ raise ValueError("must submit `key_technology_basic`")
221
+ key_technology_basic = key_technology_basic.lower()
222
+ if not cert_pem_filepath:
223
+ raise FallbackError_FilepathRequired("Must submit `cert_pem_filepath`.")
224
+ if core.openssl_version is None:
225
+ core.check_openssl_version()
226
+
227
+ spki_hash = None
228
+ # convert to DER
229
+ p1 = p2 = p3 = proc4 = None
230
+ try:
231
+ # extract the key
232
+ p1 = psutil.Popen(
233
+ [
234
+ core.openssl_path,
235
+ "x509",
236
+ "-pubkey",
237
+ "-noout",
238
+ "-in",
239
+ cert_pem_filepath,
240
+ ],
241
+ stdout=subprocess.PIPE,
242
+ stderr=subprocess.PIPE,
243
+ )
244
+ # convert to DER
245
+ p2 = psutil.Popen(
246
+ [
247
+ core.openssl_path,
248
+ key_technology_basic,
249
+ "-pubin",
250
+ "-pubout",
251
+ "-outform",
252
+ "DER",
253
+ ],
254
+ stdin=p1.stdout,
255
+ stdout=subprocess.PIPE,
256
+ stderr=subprocess.PIPE,
257
+ )
258
+ # digest
259
+ p3 = psutil.Popen(
260
+ [
261
+ core.openssl_path,
262
+ "dgst",
263
+ "-sha256",
264
+ "-binary",
265
+ ],
266
+ stdin=p2.stdout,
267
+ stdout=subprocess.PIPE,
268
+ stderr=subprocess.PIPE,
269
+ )
270
+ # encode
271
+ spki_hash = None
272
+ if as_b64:
273
+ with psutil.Popen(
274
+ [
275
+ core.openssl_path,
276
+ "enc",
277
+ "-base64",
278
+ ],
279
+ stdin=p3.stdout,
280
+ stdout=subprocess.PIPE,
281
+ stderr=subprocess.PIPE,
282
+ ) as proc4:
283
+ spki_hash, err = proc4.communicate()
284
+ if err:
285
+ raise OpenSslError("could not generate SPKI Hash")
286
+ else:
287
+ spki_hash, err = p3.communicate()
288
+ if err:
289
+ raise OpenSslError("could not generate SPKI Hash")
290
+ spki_hash = binascii.b2a_hex(spki_hash)
291
+ spki_hash = spki_hash.upper()
292
+ spki_hash = spki_hash.strip()
293
+ spki_hash = spki_hash.decode("utf8")
294
+
295
+ finally:
296
+ # Note: explicitly close what we opened
297
+ for _p in (
298
+ p1,
299
+ p2,
300
+ p3,
301
+ ):
302
+ if _p is not None:
303
+ try:
304
+ _p.stdout.close()
305
+ _p.stderr.close()
306
+ _p.terminate()
307
+ _p.wait()
308
+ except psutil.NoSuchProcess:
309
+ pass
310
+ return spki_hash
311
+
312
+
313
+ def _openssl_spki_hash_csr(
314
+ key_technology_basic: str = "",
315
+ csr_pem_filepath: str = "",
316
+ as_b64: Optional[bool] = None,
317
+ ) -> str:
318
+ """
319
+ :param key_technology_basic: Is the key an "EC" or "RSA" key?
320
+ :type key_technology_basic: str
321
+ :param csr_pem_filepath: REQUIRED filepath to PEM CSR.
322
+ Used for commandline OpenSSL fallback operations.
323
+ :type csr_pem_filepath: str
324
+ :param as_b64: Should the result be returned in Base64 encoding? default None
325
+ :type as_b64: boolean
326
+ :returns: spki_hash
327
+ :rtype: str
328
+
329
+ The OpenSSL Equivalent / Fallback is::
330
+
331
+ openssl REQ -pubkey -noout -in {CSR_FILEPATH} | \
332
+ openssl {key_technology_basic} -pubout -outform DER -pubin | \
333
+ openssl dgst -sha256 -binary | \
334
+ openssl enc -base64
335
+ """
336
+ if key_technology_basic not in ("EC", "RSA"):
337
+ raise ValueError("must submit `key_technology_basic`")
338
+ key_technology_basic = key_technology_basic.lower()
339
+ if not csr_pem_filepath:
340
+ raise FallbackError_FilepathRequired("Must submit `csr_pem_filepath`.")
341
+ if core.openssl_version is None:
342
+ core.check_openssl_version()
343
+
344
+ spki_hash = None
345
+ # convert to DER
346
+ p1 = p2 = p3 = proc4 = None
347
+ try:
348
+ # extract the key
349
+ p1 = psutil.Popen(
350
+ [
351
+ core.openssl_path,
352
+ "req",
353
+ "-pubkey",
354
+ "-noout",
355
+ "-in",
356
+ csr_pem_filepath,
357
+ ],
358
+ stdout=subprocess.PIPE,
359
+ stderr=subprocess.PIPE,
360
+ )
361
+ # convert to DER
362
+ p2 = psutil.Popen(
363
+ [
364
+ core.openssl_path,
365
+ key_technology_basic,
366
+ "-pubin",
367
+ "-pubout",
368
+ "-outform",
369
+ "DER",
370
+ ],
371
+ stdin=p1.stdout,
372
+ stdout=subprocess.PIPE,
373
+ stderr=subprocess.PIPE,
374
+ )
375
+ # digest
376
+ p3 = psutil.Popen(
377
+ [
378
+ core.openssl_path,
379
+ "dgst",
380
+ "-sha256",
381
+ "-binary",
382
+ ],
383
+ stdin=p2.stdout,
384
+ stdout=subprocess.PIPE,
385
+ stderr=subprocess.PIPE,
386
+ )
387
+ # encode
388
+ spki_hash = None
389
+ if as_b64:
390
+ with psutil.Popen(
391
+ [
392
+ core.openssl_path,
393
+ "enc",
394
+ "-base64",
395
+ ],
396
+ stdin=p3.stdout,
397
+ stdout=subprocess.PIPE,
398
+ stderr=subprocess.PIPE,
399
+ ) as proc4:
400
+ spki_hash, err = proc4.communicate()
401
+ if err:
402
+ raise OpenSslError("could not generate SPKI Hash")
403
+ else:
404
+ spki_hash, err = p3.communicate()
405
+ if err:
406
+ raise OpenSslError("could not generate SPKI Hash")
407
+ spki_hash = binascii.b2a_hex(spki_hash)
408
+ spki_hash = spki_hash.upper()
409
+ spki_hash = spki_hash.strip()
410
+ spki_hash = spki_hash.decode("utf8")
411
+ finally:
412
+ # Note: explicitly close what we opened
413
+ for _p in (
414
+ p1,
415
+ p2,
416
+ p3,
417
+ ):
418
+ if _p is not None:
419
+ try:
420
+ _p.stdout.close()
421
+ _p.stderr.close()
422
+ _p.terminate()
423
+ _p.wait()
424
+ except psutil.NoSuchProcess:
425
+ pass
426
+ return spki_hash
427
+
428
+
429
+ def _openssl_spki_hash_pkey(
430
+ key_technology_basic: str = "",
431
+ key_pem_filepath: str = "",
432
+ as_b64: Optional[bool] = None,
433
+ ) -> str:
434
+ """
435
+ :param key_technology_basic: Is the key an "EC" or "RSA" key?
436
+ :type key_technology_basic: str
437
+ :param key_pem_filepath: REQUIRED filepath to PEM encoded PrivateKey.
438
+ Used for commandline OpenSSL fallback operations.
439
+ :type key_pem_filepath: str
440
+ :param as_b64: Should the result be returned in Base64 encoding? default None
441
+ :type as_b64: boolean
442
+ :returns: spki_hash
443
+ :rtype: str
444
+
445
+ The OpenSSL Equivalent / Fallback is::
446
+
447
+ openssl rsa -in {KEY_FILEPATH} -pubout -outform der | \
448
+ openssl dgst -sha256 -binary | \
449
+ openssl enc -base64
450
+ """
451
+ if key_technology_basic not in ("EC", "RSA"):
452
+ raise ValueError("must submit `key_technology_basic`")
453
+ key_technology_basic = key_technology_basic.lower()
454
+ if not key_pem_filepath:
455
+ raise FallbackError_FilepathRequired("Must submit `key_pem_filepath`.")
456
+ if core.openssl_version is None:
457
+ core.check_openssl_version()
458
+
459
+ spki_hash = None
460
+ # convert to DER
461
+ p1 = p2 = proc3 = None
462
+ try:
463
+ # convert to DER
464
+ p1 = psutil.Popen(
465
+ [
466
+ core.openssl_path,
467
+ key_technology_basic,
468
+ "-pubout",
469
+ "-outform",
470
+ "DER",
471
+ "-in",
472
+ key_pem_filepath,
473
+ ],
474
+ stdout=subprocess.PIPE,
475
+ stderr=subprocess.PIPE,
476
+ )
477
+ # digest
478
+ p2 = psutil.Popen(
479
+ [
480
+ core.openssl_path,
481
+ "dgst",
482
+ "-sha256",
483
+ "-binary",
484
+ ],
485
+ stdin=p1.stdout,
486
+ stdout=subprocess.PIPE,
487
+ stderr=subprocess.PIPE,
488
+ )
489
+ # encode
490
+ if as_b64:
491
+ with psutil.Popen(
492
+ [
493
+ core.openssl_path,
494
+ "enc",
495
+ "-base64",
496
+ ],
497
+ stdin=p2.stdout,
498
+ stdout=subprocess.PIPE,
499
+ stderr=subprocess.PIPE,
500
+ ) as proc3:
501
+ spki_hash, err = proc3.communicate()
502
+ if err:
503
+ raise OpenSslError("could not generate SPKI Hash")
504
+ else:
505
+ spki_hash, err = p2.communicate()
506
+ if err:
507
+ raise OpenSslError("could not generate SPKI Hash")
508
+ spki_hash = binascii.b2a_hex(spki_hash)
509
+ spki_hash = spki_hash.upper()
510
+ spki_hash = spki_hash.strip()
511
+ spki_hash = spki_hash.decode("utf8")
512
+ finally:
513
+ # Note: explicitly close what we opened
514
+ for _p in (
515
+ p1,
516
+ p2,
517
+ ):
518
+ if _p is not None:
519
+ try:
520
+ _p.stdout.close()
521
+ _p.stderr.close()
522
+ _p.terminate()
523
+ _p.wait()
524
+ except psutil.NoSuchProcess:
525
+ pass
526
+ return spki_hash
527
+
528
+
529
+ def _openssl_cert_single_op__pem(
530
+ cert_pem: str,
531
+ single_op: str,
532
+ ) -> str:
533
+ """
534
+ this just invokes `_openssl_cert_single_op__pem_filepath` with a tempfile
535
+ """
536
+ _tmpfile_pem = new_pem_tempfile(cert_pem)
537
+ try:
538
+ cert_pem_filepath = _tmpfile_pem.name
539
+ return _openssl_cert_single_op__pem_filepath(cert_pem_filepath, single_op)
540
+ except Exception as exc: # noqa: F841
541
+ raise
542
+ finally:
543
+ _tmpfile_pem.close()
544
+
545
+
546
+ def _openssl_cert_single_op__pem_filepath(
547
+ pem_filepath: str,
548
+ single_op: str,
549
+ ) -> str:
550
+ """
551
+ handles a single pem operation to `openssl x509`
552
+
553
+ :param pem_filepath: filepath to pem encoded cert
554
+ :type pem_filepath: str
555
+ :param single_op: operation
556
+ :type single_op: str
557
+ :returns: openssl output
558
+ :rtype: str
559
+
560
+ openssl x509 -noout -issuer -in cert.pem
561
+ openssl x509 -noout -issuer_hash -in cert.pem
562
+
563
+ openssl x509 -noout -issuer_hash -in {CERT}
564
+ returns the data found in
565
+ X509v3 extensions:
566
+ X509v3 Authority Key Identifier:
567
+ keyid:{VALUE}
568
+
569
+ openssl x509 -noout -subject_hash -in {CERT}
570
+ returns the data found in
571
+ X509v3 extensions:
572
+ X509v3 Subject Key Identifier:
573
+ {VALUE}
574
+
575
+ The OpenSSL Equivalent / Fallback is::
576
+
577
+ openssl x509 -noout {OPERATION} -in {FILEPATH})
578
+ """
579
+ if single_op not in (
580
+ "-issuer_hash",
581
+ "-issuer",
582
+ "-subject_hash",
583
+ "-subject",
584
+ "-startdate",
585
+ "-enddate",
586
+ ):
587
+ raise ValueError("invalid `single_op`")
588
+ if not pem_filepath:
589
+ raise FallbackError_FilepathRequired("Must submit `pem_filepath`.")
590
+ if core.openssl_version is None:
591
+ core.check_openssl_version()
592
+
593
+ with psutil.Popen(
594
+ [core.openssl_path, "x509", "-noout", single_op, "-in", pem_filepath],
595
+ stdout=subprocess.PIPE,
596
+ stderr=subprocess.PIPE,
597
+ ) as proc:
598
+ data_bytes, err = proc.communicate()
599
+ if not data_bytes:
600
+ raise OpenSslError_InvalidCertificate(err)
601
+ data_str = data_bytes.decode("utf8")
602
+ data_str = data_str.strip()
603
+ return data_str
604
+
605
+
606
+ def cert_ext__pem_filepath(
607
+ pem_filepath: str,
608
+ ext: str,
609
+ ) -> str:
610
+ """
611
+ handles a single pem operation to `openssl x509` with EXTENSION
612
+ /usr/local/bin/openssl x509 -noout -ext subjectAltName -in cert.pem
613
+ /usr/local/bin/openssl x509 -noout -ext authorityKeyIdentifier -in cert.pem
614
+ /usr/local/bin/openssl x509 -noout -ext authorityInfoAccess -in cert.pem
615
+
616
+ :param pem_filepath: filepath to the PEM encoded Certificate
617
+ :type pem_filepath: str
618
+ :param ext: a supported x509 extension
619
+ :type ext: str
620
+ :returns: openssl output value
621
+ :rtype: str
622
+
623
+ The OpenSSL Equivalent / Fallback is::
624
+
625
+ openssl x509 -noout -ext {EXT} -in {FILEPATH})
626
+ """
627
+ if ext not in ("subjectAltName", "authorityKeyIdentifier", "authorityInfoAccess"):
628
+ raise ValueError("invalid `ext`")
629
+ if not pem_filepath:
630
+ raise FallbackError_FilepathRequired("Must submit `pem_filepath`.")
631
+ if core.openssl_version is None:
632
+ core.check_openssl_version()
633
+
634
+ with psutil.Popen(
635
+ [core.openssl_path, "x509", "-noout", "-ext", ext, "-in", pem_filepath],
636
+ stdout=subprocess.PIPE,
637
+ stderr=subprocess.PIPE,
638
+ ) as proc:
639
+ data_bytes, err = proc.communicate()
640
+ if not data_bytes:
641
+ raise OpenSslError_InvalidCertificate(err)
642
+ data_str = data_bytes.decode("utf8")
643
+ data_str = data_str.strip()
644
+ return data_str
645
+
646
+
647
+ def csr_single_op__pem_filepath(
648
+ pem_filepath: str,
649
+ single_op: str,
650
+ ) -> str:
651
+ """
652
+ handles a single pem operation to `openssl req` with EXTENSION
653
+
654
+ openssl req -noout -subject -in csr.pem
655
+
656
+ :param pem_filepath: filepath to the PEM encoded CSR.
657
+ :type pem_filepath: str
658
+ :param single_op: a supported `openssl req` operation
659
+ :type single_op: str
660
+ :returns: openssl output value
661
+ :rtype: str
662
+
663
+ The OpenSSL Equivalent / Fallback is::
664
+
665
+ openssl req -noout {OPERATION} -in {FILEPATH})
666
+ """
667
+ if single_op not in ("-subject",):
668
+ raise ValueError("invalid `single_op`")
669
+ if not pem_filepath:
670
+ raise FallbackError_FilepathRequired("Must submit `pem_filepath`.")
671
+ if core.openssl_version is None:
672
+ core.check_openssl_version()
673
+
674
+ with psutil.Popen(
675
+ [core.openssl_path, "req", "-noout", single_op, "-in", pem_filepath],
676
+ stdout=subprocess.PIPE,
677
+ stderr=subprocess.PIPE,
678
+ ) as proc:
679
+ data_bytes, err = proc.communicate()
680
+ if not data_bytes:
681
+ raise OpenSslError_InvalidCSR(err)
682
+ data_str = data_bytes.decode("utf8")
683
+ data_str = data_str.strip()
684
+ return data_str
685
+
686
+
687
+ def key_single_op__pem_filepath(
688
+ keytype: str = "RSA",
689
+ pem_filepath: str = "",
690
+ single_op: str = "",
691
+ ) -> str:
692
+ """
693
+ :param keytype: the type of key: RSA or EC
694
+ :type keytype: str
695
+ :param pem_filepath: filepath to the PEM encoded Key
696
+ :type pem_filepath: str
697
+ :param single_op: a supported `openssl rsa/ec` operation
698
+ :type single_op: str
699
+ :returns: openssl output value
700
+ :rtype: str
701
+
702
+ THIS SHOULD NOT BE USED BY INTERNAL CODE
703
+
704
+ This is a bit odd...
705
+
706
+ 1. If "-check" is okay (or reading is okay), there may be no output on stdout
707
+ HOWEVER
708
+ the read message (success) may happen on stderr
709
+ 2. If openssl can't read the file, it will raise an exception
710
+
711
+ earlier versions of openssl DO NOT HAVE `ec --check`
712
+ current versions do
713
+
714
+ The OpenSSL Equivalent / Fallback is::
715
+
716
+ openssl {KEYTYPE} -noout {OPERATION} -in {FILEPATH})
717
+
718
+ Such as:
719
+
720
+ openssl rsa -noout -check -in {KEY}
721
+ openssl rsa -noout -modulus -in {KEY}
722
+ openssl rsa -noout -text -in {KEY}
723
+
724
+ openssl ec -noout -in {KEY}
725
+ openssl ec -noout -modulus -in {KEY}
726
+ openssl ec -noout -text -in {KEY}
727
+ """
728
+ if keytype not in ("RSA", "EC"):
729
+ raise ValueError("keytype must be `RSA or EC`")
730
+ if single_op not in ("-check", "-modulus", "-text"):
731
+ raise ValueError("invalid `single_op`")
732
+ if not pem_filepath:
733
+ raise FallbackError_FilepathRequired("Must submit `pem_filepath`.")
734
+ if core.openssl_version is None:
735
+ core.check_openssl_version()
736
+
737
+ with psutil.Popen(
738
+ [core.openssl_path, keytype.lower(), "-noout", single_op, "-in", pem_filepath],
739
+ stdout=subprocess.PIPE,
740
+ stderr=subprocess.PIPE,
741
+ ) as proc:
742
+ data_bytes, err = proc.communicate()
743
+ if not data_bytes:
744
+ if err.startswith(b"unknown option -check"):
745
+ raise OpenSslError_VersionTooLow(err)
746
+ elif proc.returncode or err not in (
747
+ b"read EC key\nEC Key valid.\n",
748
+ b"EC Key valid.\n",
749
+ ):
750
+ # this happens, where some versions give an error and no data!
751
+ raise OpenSslError_InvalidKey(err)
752
+ data_str = data_bytes.decode("utf8")
753
+ data_str = data_str.strip()
754
+ return data_str
755
+
756
+
757
+ def san_domains_from_text(text: str) -> List[str]:
758
+ """
759
+ Helper function to extract SAN domains from a chunk of text in a x509 object
760
+
761
+ :param text: string extracted from a x509 document
762
+ :type text: str
763
+ :returns: list of domains
764
+ :rtype: list
765
+ """
766
+ san_domains = set([])
767
+ _subject_alt_names = RE_openssl_x509_san.search(text)
768
+ if _subject_alt_names is not None:
769
+ for _san in _subject_alt_names.group(1).split(", "):
770
+ if _san.startswith("DNS:"):
771
+ san_domains.add(_san[4:].lower())
772
+ return sorted(list(san_domains))
773
+
774
+
775
+ def authority_key_identifier_from_text(text: str) -> Optional[str]:
776
+ """
777
+ :param text: string extracted from a x509 document
778
+ :type text: str
779
+ :returns: authority_key_identifier
780
+ :rtype: str
781
+
782
+ openssl will print a uppercase hex pairs, separated by a colon
783
+ we should remove the colons
784
+ """
785
+ results = RE_openssl_x509_authority_key_identifier.findall(text)
786
+ if results:
787
+ authority_key_identifier = results[0]
788
+ # ensure we have a key_id and not "URI:" or other convention
789
+ if authority_key_identifier[2] == ":":
790
+ return authority_key_identifier.replace(":", "")
791
+ return None
792
+
793
+
794
+ def serial_from_text(text: str) -> Optional[int]:
795
+ """
796
+ :param text: string extracted from a x509 document
797
+ :type text: str
798
+ :returns: serial
799
+ :rtype: int
800
+ """
801
+ results = RE_openssl_x509_serial.findall(text)
802
+ if results:
803
+ serial = results[0]
804
+ return int(serial)
805
+ return None
806
+
807
+
808
+ def issuer_uri_from_text(text: str) -> Optional[str]:
809
+ """
810
+ :param text: string extracted from a x509 document
811
+ :type text: str
812
+ :returns: issuer_uri
813
+ :rtype: str
814
+ """
815
+ results = RE_openssl_x509_issuer_uri.findall(text)
816
+ if results:
817
+ return results[0]
818
+ return None
819
+
820
+
821
+ def _x509_pubkey_technology__text(cert_text: str) -> Optional[TECHNOLOGY_RETURN_VALUES]:
822
+ m = RE_openssl_x509_keytype_rsa.search(cert_text)
823
+ if m:
824
+ v = m.groups()
825
+ return ("RSA", (int(v[0]),))
826
+ m = RE_openssl_x509_keytype_ec.search(cert_text)
827
+ if m:
828
+ v = m.groups()
829
+ c = None
830
+ if v[2]:
831
+ c = v[2]
832
+ elif v[0]:
833
+ c = "P-%s" % v[0]
834
+ else:
835
+ # TODO? better parsing
836
+ return None
837
+ return ("EC", (c,))
838
+ return None
839
+
840
+
841
+ def _cert_pubkey_technology__text(cert_text: str) -> Optional[TECHNOLOGY_RETURN_VALUES]:
842
+ """
843
+ :param cert_text: string extracted from a x509 document
844
+ :type cert_text: str
845
+ :returns: Pubkey type: "RSA" or "EC"
846
+ :rtype: strfRE_openssl_x509_keytype_rsa
847
+ """
848
+ # `cert_text` is the output of of `openssl x509 -noout -text -in MYCERT `
849
+ return _x509_pubkey_technology__text(cert_text)
850
+
851
+
852
+ def _csr_pubkey_technology__text(csr_text: str) -> Optional[TECHNOLOGY_RETURN_VALUES]:
853
+ """
854
+ :param csr_text: string extracted from a CSR document
855
+ :type csr_text: str
856
+ :returns: Pubkey type: "RSA" or "EC"
857
+ :rtype: str
858
+ """
859
+ # `csr_text` is the output of of `openssl req -noout -text -in MYCERT`
860
+ return _x509_pubkey_technology__text(csr_text)