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
cert_utils/core.py ADDED
@@ -0,0 +1,3075 @@
1
+ # stdlib
2
+ import base64
3
+ import binascii
4
+ import datetime
5
+ import hashlib
6
+ import ipaddress
7
+ import json
8
+ import logging
9
+ import os
10
+ import re
11
+ import subprocess
12
+ import tempfile
13
+ from typing import Any
14
+ from typing import Dict
15
+ from typing import List
16
+ from typing import Optional
17
+ from typing import Tuple
18
+ from typing import TYPE_CHECKING
19
+ from typing import Union
20
+
21
+ # pypi
22
+ from dateutil import parser as dateutil_parser
23
+ import psutil
24
+ from typing_extensions import Literal
25
+
26
+ # localapp
27
+ from . import conditionals
28
+ from .compat.cryptography import _cryptography__cert_or_req__all_names
29
+ from .compat.cryptography import _cryptography__public_key_spki_sha256
30
+ from .compat.cryptography import _cryptography__public_key_technology
31
+ from .compat.cryptography import _format_cryptography_components
32
+ from .compat.cryptography import cryptography__cert_and_chain_from_fullchain
33
+ from .compat.openssl import _cert_pubkey_technology__text
34
+ from .compat.openssl import _cleanup_openssl_modulus
35
+ from .compat.openssl import _csr_pubkey_technology__text
36
+ from .compat.openssl import _format_openssl_components
37
+ from .compat.openssl import _openssl_cert__normalize_pem
38
+ from .compat.openssl import _openssl_cert_single_op__pem_filepath
39
+ from .compat.openssl import _openssl_spki_hash_cert
40
+ from .compat.openssl import _openssl_spki_hash_csr
41
+ from .compat.openssl import _openssl_spki_hash_pkey
42
+ from .compat.openssl import authority_key_identifier_from_text
43
+ from .compat.openssl import cert_ext__pem_filepath
44
+ from .compat.openssl import csr_single_op__pem_filepath
45
+ from .compat.openssl import issuer_uri_from_text
46
+ from .compat.openssl import key_single_op__pem_filepath
47
+ from .compat.openssl import RE_openssl_x509_subject
48
+ from .compat.openssl import san_domains_from_text
49
+ from .compat.openssl import serial_from_text
50
+ from .compat.signatures import der_to_raw as _ecdsa_der_to_raw
51
+ from .compat.signatures import openssl_key_width as _openssl_account_key_width
52
+ from .compat.signatures import raw_to_der as _ecdsa_raw_to_der
53
+ from .errors import CryptographyError
54
+ from .errors import FallbackError_FilepathRequired
55
+ from .errors import OpenSslError
56
+ from .errors import OpenSslError_CsrGeneration
57
+ from .errors import OpenSslError_InvalidCertificate
58
+ from .errors import OpenSslError_InvalidCSR
59
+ from .errors import OpenSslError_InvalidKey
60
+ from .errors import OpenSslError_InvalidSignature
61
+ from .errors import OpenSslError_VersionTooLow
62
+ from .model import ALLOWED_BITS_RSA
63
+ from .model import ALLOWED_CURVES_ECDSA
64
+ from .model import CertificateData
65
+ from .model import CSRData
66
+ from .model import KeyData
67
+ from .model import KeyTechnology
68
+ from .model import KeyTechnologyEnum
69
+ from .utils import cleanup_pem_text
70
+ from .utils import convert_binary_to_hex
71
+ from .utils import curve_to_nist
72
+ from .utils import jose_b64
73
+ from .utils import new_pem_tempfile
74
+ from .utils import split_pem_chain
75
+ from .utils import TECHNOLOGY_RETURN_VALUES
76
+
77
+ if TYPE_CHECKING:
78
+ from cryptography.hazmat.primitives.asymmetric.dsa import DSAPublicKey
79
+ from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
80
+ from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
81
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
82
+ from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey
83
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
84
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
85
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
86
+ from cryptography.hazmat.primitives.asymmetric.x448 import X448PublicKey
87
+ from cryptography.hazmat.primitives.hashes import HashAlgorithm
88
+ from cryptography.x509 import Certificate
89
+ from cryptography.x509 import CertificateSigningRequest
90
+ from typing_extensions import Protocol
91
+
92
+ class _PublicKeySPKI(Protocol):
93
+ def public_bytes(self, encoding: Any, format: Any) -> bytes: ...
94
+
95
+ _TYPES_CRYPTOGRAPHY_KEYS = Union[
96
+ EllipticCurvePrivateKey,
97
+ EllipticCurvePublicKey,
98
+ RSAPrivateKey,
99
+ RSAPublicKey,
100
+ ]
101
+ _TYPES_CRYPTOGRAPHY_PRIVATEKEY = Union[
102
+ EllipticCurvePrivateKey,
103
+ RSAPrivateKey,
104
+ ]
105
+ _TYPES_CRYPTOGRAPHY_PUBLICKEY = Union[
106
+ EllipticCurvePublicKey,
107
+ RSAPublicKey,
108
+ ]
109
+ _TYPES_CRYPTOGRAPHY_PUBLICKEY_EXTENDED = Union[
110
+ DSAPublicKey,
111
+ EllipticCurvePublicKey,
112
+ Ed25519PublicKey,
113
+ Ed448PublicKey,
114
+ RSAPublicKey,
115
+ X25519PublicKey,
116
+ X448PublicKey,
117
+ ]
118
+
119
+
120
+ # ------------------------------------------------------------------------------
121
+
122
+ NEEDS_TEMPFILES = True
123
+ if conditionals.cryptography and conditionals.josepy:
124
+ NEEDS_TEMPFILES = False
125
+
126
+ # ==============================================================================
127
+
128
+ log = logging.getLogger("cert_utils")
129
+ log.setLevel(logging.INFO)
130
+
131
+ # ------------------------------------------------------------------------------
132
+
133
+
134
+ # set these as vars, so other packages can programatticaly test the env for conflicts
135
+ _envvar_SSL_BIN_OPENSSL = "SSL_BIN_OPENSSL"
136
+ _envvar_SSL_CONF_OPENSSL = "SSL_CONF_OPENSSL"
137
+
138
+ openssl_path = os.environ.get(_envvar_SSL_BIN_OPENSSL, None) or "openssl"
139
+ openssl_path_conf = (
140
+ os.environ.get(_envvar_SSL_CONF_OPENSSL, None) or "/etc/ssl/openssl.cnf"
141
+ )
142
+
143
+ ACME_VERSION = "v2"
144
+ openssl_version: Optional[List[int]] = None
145
+ _RE_openssl_version = re.compile(r"OpenSSL ((\d+\.\d+\.\d+)\w*) ", re.I)
146
+ _openssl_behavior: Optional[str] = None # 'a' or 'b'
147
+
148
+
149
+ # If True, will:
150
+ # * disable SSL Verification
151
+ # * disable HTTP Challenge pre-Read
152
+ TESTING_ENVIRONMENT = False
153
+
154
+ # LetsEncrypt max
155
+ MAX_DOMAINS_PER_CERTIFICATE = 100
156
+
157
+
158
+ def update_from_appsettings(appsettings: Dict[str, Any]) -> None:
159
+ """
160
+ update the module data based on settings
161
+
162
+ :param appsettings: a dict containing the Pyramid application settings
163
+ :type appsettings: dict or dict-like
164
+ """
165
+ global openssl_path
166
+ global openssl_path_conf
167
+ # but first check for conflicts
168
+ # was the env set?
169
+ _openssl_env = os.environ.get(_envvar_SSL_BIN_OPENSSL, None) or os.environ.get(
170
+ _envvar_SSL_CONF_OPENSSL, None
171
+ )
172
+ # was the ini set?
173
+ _openssl_ini = appsettings.get("openssl_path", None) or appsettings.get(
174
+ "openssl_path_conf", None
175
+ )
176
+ if _openssl_env and _openssl_ini:
177
+ raise ValueError("OpenSSL values specified in .ini and environment")
178
+ # did we set the ini?
179
+ _changed_openssl = False
180
+ if "openssl_path" in appsettings:
181
+ openssl_path = appsettings["openssl_path"]
182
+ _changed_openssl = True
183
+ if "openssl_path_conf" in appsettings:
184
+ openssl_path_conf = appsettings["openssl_path_conf"]
185
+ _changed_openssl = True
186
+ if _changed_openssl:
187
+ check_openssl_version(replace=True)
188
+
189
+
190
+ # ==============================================================================
191
+
192
+
193
+ EXTENSION_TO_MIME = {
194
+ "pem": {
195
+ "*": "application/x-pem-file",
196
+ },
197
+ "cer": {
198
+ "*": "application/pkix-cert",
199
+ },
200
+ "crt": {
201
+ "CertificateCA": "application/x-x509-ca-cert",
202
+ "CertificateSigned": "application/x-x509-server-cert",
203
+ },
204
+ "p7c": {
205
+ "*": "application/pkcs7-mime",
206
+ },
207
+ "der": {
208
+ "CertificateCA": "application/x-x509-ca-cert",
209
+ "CertificateSigned": "application/x-x509-server-cert",
210
+ },
211
+ "key": {
212
+ "*": "application/pkcs8",
213
+ },
214
+ }
215
+
216
+
217
+ # ==============================================================================
218
+
219
+
220
+ def check_openssl_version(replace: bool = False) -> List[int]:
221
+ """
222
+ :param replace: should this run if the value is already known? (default False)
223
+ :type replace: boolean
224
+ :returns: current openssl version on the commandline
225
+ :rtype: str
226
+ """
227
+ global openssl_version
228
+ global _openssl_behavior
229
+ if (openssl_version is None) or replace:
230
+ with psutil.Popen(
231
+ [
232
+ openssl_path,
233
+ "version",
234
+ ],
235
+ stdout=subprocess.PIPE,
236
+ stderr=subprocess.PIPE,
237
+ ) as proc:
238
+ data_bytes, err = proc.communicate()
239
+ if err:
240
+ raise OpenSslError("could not check version")
241
+ version_text = data_bytes.decode("utf8")
242
+ # version_text will be something like "OpenSSL 1.0.2g 1 Mar 2016\n"
243
+ # version_text.strip().split(' ')[1] == '1.0.2g'
244
+ # but... regex!
245
+ m = _RE_openssl_version.search(version_text)
246
+ if not m:
247
+ raise ValueError(
248
+ "Could not regex OpenSSL",
249
+ "openssl_path: %s" % openssl_path,
250
+ "version: %s" % version_text,
251
+ )
252
+ # m.groups == ('1.0.2g', '1.0.2')
253
+ v = m.groups()[1]
254
+ v = [int(i) for i in v.split(".")]
255
+ openssl_version = v
256
+ _openssl_behavior = "a" # default to old behavior
257
+ # OpenSSL 1.1.1 doesn't need a tempfile for SANs
258
+ if (v[0] >= 1) and (v[1] >= 1) and (v[2] >= 1):
259
+ _openssl_behavior = "b"
260
+ elif v[0] == 3:
261
+ # some regex are different, but the behavior should be the same
262
+ _openssl_behavior = "b"
263
+ return openssl_version
264
+
265
+
266
+ # ==============================================================================
267
+
268
+
269
+ def make_csr(
270
+ domain_names: Optional[List[str]] = None,
271
+ key_pem: Optional[str] = None,
272
+ key_pem_filepath: Optional[str] = None,
273
+ ipaddrs: Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]] = None,
274
+ must_staple: bool = False,
275
+ max_domains: int = MAX_DOMAINS_PER_CERTIFICATE,
276
+ ) -> str:
277
+ """
278
+ This routine will use cryptography if available.
279
+ If not, openssl is used via subprocesses
280
+
281
+ :param domain_names: a list of fully qualified domain names
282
+ :type domain_names: list of strings
283
+ :param key_pem: a PEM encoded PrivateKey
284
+ :type key_pem: str
285
+ :param key_pem_filepath: Optional filepath to the PEM encoded PrivateKey.
286
+ Only used for commandline OpenSSL fallback operations.
287
+ :type key_pem_filepath: str
288
+ :param ipaddrs: a list of ip addresses
289
+ :type ipaddrs: list of strings
290
+ :param bool must_staple: Whether to include the TLS Feature extension (aka
291
+ OCSP Must Staple: https://tools.ietf.org/html/rfc7633).
292
+
293
+ :param int max_domains: the max domains to put on a certificate;
294
+ defaults to package max
295
+
296
+ :returns: CSR, likely PEM encoded
297
+ :rtype: str
298
+
299
+ The OpenSSL Equivalent / Fallback is::
300
+
301
+ openssl req -new -sha256 -k {FILEPATH_KEY} -subj "/CN=example.com"
302
+ ===
303
+ vi FILEPATH_SAN
304
+ [SAN]\nsubjectAltName=DNS:example.com,DNS:www.example.com
305
+ openssl req -new -sha256 -k {FILEPATH_KEY} -subj "/" -reqexts SAN -config < /bin/cat {FILEPATH_SAN}
306
+ ===
307
+ vi FILEPATH_SAN
308
+ subjectAltName=DNS:example.com,DNS:www.example.com
309
+ openssl req -new -sha256 -k {FILEPATH_KEY} -subj "/" -addext {FILEPATH_SAN}
310
+ """
311
+ log.info("make_csr >")
312
+ # General CSR size guard, not an issuer/profile-specific rate limit.
313
+
314
+ if domain_names is None:
315
+ domain_names = []
316
+ if ipaddrs is None:
317
+ ipaddrs = []
318
+ if len(domain_names) + len(ipaddrs) == 0:
319
+ raise OpenSslError_CsrGeneration(
320
+ "At least one of domains or ipaddrs parameter need to be not empty"
321
+ )
322
+ elif len(domain_names) + len(ipaddrs) > max_domains:
323
+ raise OpenSslError_CsrGeneration(
324
+ "LetsEncrypt can only allow `%s` domains per certificate"
325
+ )
326
+
327
+ # first try with python
328
+ if conditionals.cryptography:
329
+ if TYPE_CHECKING:
330
+ assert conditionals.crypto_dsa is not None
331
+ assert conditionals.crypto_ec is not None
332
+ assert conditionals.crypto_ed25519 is not None
333
+ assert conditionals.crypto_ed448 is not None
334
+ assert conditionals.crypto_hashes is not None
335
+ assert conditionals.crypto_rsa is not None
336
+ assert conditionals.crypto_serialization is not None
337
+
338
+ if not key_pem:
339
+ raise ValueError("Must submit `key_pem`")
340
+
341
+ private_key = conditionals.crypto_serialization.load_pem_private_key(
342
+ key_pem.encode(), password=None
343
+ )
344
+
345
+ if not isinstance(
346
+ private_key,
347
+ (
348
+ conditionals.crypto_dsa.DSAPrivateKey,
349
+ conditionals.crypto_ec.EllipticCurvePrivateKey,
350
+ conditionals.crypto_ed25519.Ed25519PrivateKey,
351
+ conditionals.crypto_ed448.Ed448PrivateKey,
352
+ conditionals.crypto_rsa.RSAPrivateKey,
353
+ ),
354
+ ):
355
+ raise ValueError(f"Invalid private key type: {type(private_key)}")
356
+
357
+ builder = (
358
+ conditionals.cryptography.x509.CertificateSigningRequestBuilder()
359
+ .subject_name(conditionals.cryptography.x509.Name([]))
360
+ .add_extension(
361
+ conditionals.cryptography.x509.SubjectAlternativeName(
362
+ [conditionals.cryptography.x509.DNSName(d) for d in domain_names]
363
+ + [conditionals.cryptography.x509.IPAddress(i) for i in ipaddrs]
364
+ ),
365
+ critical=False,
366
+ )
367
+ )
368
+ if must_staple:
369
+ builder = builder.add_extension(
370
+ conditionals.cryptography.x509.TLSFeature(
371
+ [conditionals.cryptography.x509.TLSFeatureType.status_request]
372
+ ),
373
+ critical=False,
374
+ )
375
+
376
+ csr = builder.sign(private_key, conditionals.crypto_hashes.SHA256())
377
+ pem_bytes = csr.public_bytes(conditionals.crypto_serialization.Encoding.PEM)
378
+ return pem_bytes.decode()
379
+
380
+ log.debug(".make_csr > openssl fallback")
381
+ if key_pem_filepath is None:
382
+ # TODO: generate a tempfile?
383
+ raise FallbackError_FilepathRequired("Must submit `key_pem_filepath`.")
384
+ if openssl_version is None:
385
+ check_openssl_version()
386
+
387
+ _acme_generator_strategy = None
388
+ if ACME_VERSION == "v1":
389
+ if len(domain_names) == 1:
390
+ _acme_generator_strategy = 1
391
+ else:
392
+ _acme_generator_strategy = 2
393
+ elif ACME_VERSION == "v2":
394
+ _acme_generator_strategy = 2
395
+
396
+ if _acme_generator_strategy == 1:
397
+ """
398
+ This is the ACME-V1 method for single domain Certificates
399
+ * the Certificate's subject (commonName) is `/CN=yourdomain`
400
+ """
401
+ _csr_subject = "/CN=%s" % domain_names[0]
402
+ with psutil.Popen(
403
+ [
404
+ openssl_path,
405
+ "req",
406
+ "-new",
407
+ "-sha256",
408
+ "-key",
409
+ key_pem_filepath,
410
+ "-subj",
411
+ _csr_subject,
412
+ ],
413
+ stdout=subprocess.PIPE,
414
+ stderr=subprocess.PIPE,
415
+ ) as proc:
416
+ data_bytes, err = proc.communicate()
417
+ if proc.returncode:
418
+ raise OpenSslError_CsrGeneration(err.decode("utf8", errors="replace"))
419
+ csr_text = data_bytes.decode("utf8")
420
+
421
+ elif _acme_generator_strategy == 2:
422
+ """
423
+ This is the ACME-V2 method for single domain Certificates. It works on ACME-V1.
424
+ * the Certificate's subject (commonName) is `/`
425
+ * ALL domains appear in subjectAltName
426
+
427
+ The ACME Spec allows for the domain to be provided in:
428
+ * commonName
429
+ * SAN
430
+ * both
431
+
432
+ LetsEncrypt interpreted the relevant passage as not requiring the server to accept each of these.
433
+ """
434
+
435
+ # getting subprocess to work right is a pain, because we need to chain a bunch of commands
436
+ # to get around this, we'll do two things:
437
+ # 1. cat the [SAN] and openssl path file onto a tempfile
438
+ # 2. use shell=True
439
+
440
+ domain_names = sorted(domain_names)
441
+
442
+ # the subject should be /, which will become the serial number
443
+ # see https://community.letsencrypt.org/t/certificates-with-serialnumber-in-subject/11891
444
+ _csr_subject = "/"
445
+
446
+ if _openssl_behavior == "a":
447
+ # earlier OpenSSL versions require us to pop in the subjectAltName via a cat'd file
448
+
449
+ # generate the [SAN]
450
+ _csr_san = "[SAN]\nsubjectAltName=" + ",".join(
451
+ ["DNS:%s" % d for d in domain_names]
452
+ )
453
+
454
+ # store some data in a tempfile
455
+ with open(openssl_path_conf, "rt", encoding="utf-8") as _f_conf:
456
+ _conf_data = _f_conf.read()
457
+
458
+ _newline = "\n\n"
459
+ with tempfile.NamedTemporaryFile() as tmpfile_csr_san:
460
+ # `.encode()` to bytes
461
+ tmpfile_csr_san.write(_conf_data.encode())
462
+ tmpfile_csr_san.write(_newline.encode())
463
+ tmpfile_csr_san.write(_csr_san.encode())
464
+ tmpfile_csr_san.seek(0)
465
+
466
+ # note that we use /bin/cat (!)
467
+ _command_legacy = [
468
+ openssl_path,
469
+ "req",
470
+ "-new",
471
+ "-sha256",
472
+ "-key",
473
+ key_pem_filepath,
474
+ "-subj",
475
+ "/",
476
+ "-reqexts",
477
+ "SAN",
478
+ "-config",
479
+ tmpfile_csr_san.name,
480
+ ]
481
+ with psutil.Popen(
482
+ _command_legacy, stdout=subprocess.PIPE, stderr=subprocess.PIPE
483
+ ) as proc:
484
+ data_bytes, err = proc.communicate()
485
+ if proc.returncode:
486
+ raise OpenSslError_CsrGeneration(
487
+ err.decode("utf8", errors="replace")
488
+ )
489
+ csr_text = data_bytes.decode("utf8")
490
+ csr_text = cleanup_pem_text(csr_text)
491
+
492
+ elif _openssl_behavior == "b":
493
+ # new OpenSSL versions support passing in the `subjectAltName` via the commandline
494
+
495
+ # generate the [SAN]
496
+ _csr_san = "subjectAltName = " + ", ".join(
497
+ ["DNS:%s" % d for d in domain_names]
498
+ )
499
+ _command_args = [
500
+ openssl_path,
501
+ "req",
502
+ "-new",
503
+ "-sha256",
504
+ "-key",
505
+ key_pem_filepath,
506
+ "-subj",
507
+ "/",
508
+ "-addext",
509
+ _csr_san,
510
+ ]
511
+ with psutil.Popen(
512
+ _command_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
513
+ ) as proc:
514
+ data_bytes, err = proc.communicate()
515
+ if proc.returncode:
516
+ raise OpenSslError_CsrGeneration(
517
+ err.decode("utf8", errors="replace")
518
+ )
519
+ csr_text = data_bytes.decode("utf8")
520
+ csr_text = cleanup_pem_text(csr_text)
521
+ else:
522
+ raise OpenSslError_CsrGeneration("invalid ACME generator")
523
+
524
+ return csr_text
525
+
526
+
527
+ def parse_cert__domains(
528
+ cert_pem: str,
529
+ cert_pem_filepath: Optional[str] = None,
530
+ ) -> List[str]:
531
+ """
532
+ gets ALL domains from a Certificate
533
+ * san (subjectAlternateName)
534
+ * subject (commonName)
535
+
536
+ This routine will use cryptography if available.
537
+ If not, openssl is used via subprocesses
538
+
539
+ :param cert_pem: a PEM encoded Certificate
540
+ :type cert_pem: str
541
+ :param cert_pem_filepath: Optional filepath to the PEM encoded Certificate.
542
+ Only used for commandline OpenSSL fallback operations.
543
+ :type cert_pem_filepath: str
544
+ :returns: List of Fully Qualified Domain Names (str) in the Certificate
545
+ :rtype: list
546
+
547
+ The OpenSSL Equivalent / Fallback is::
548
+
549
+ openssl x509 -in {FILEPATH} -noout -text
550
+ """
551
+ log.info("parse_cert__domains >")
552
+ if conditionals.cryptography:
553
+ cert = conditionals.cryptography.x509.load_pem_x509_certificate(
554
+ cert_pem.encode()
555
+ )
556
+ return _cryptography__cert_or_req__all_names(cert)
557
+
558
+ log.debug(".parse_cert__domains > openssl fallback")
559
+ # fallback onto OpenSSL
560
+ # `openssl x509 -in MYCERT -noout -text`
561
+ if cert_pem_filepath is None:
562
+ # TODO: generate a tempfile?
563
+ raise FallbackError_FilepathRequired("Must submit `cert_pem_filepath`.")
564
+ if openssl_version is None:
565
+ check_openssl_version()
566
+
567
+ with psutil.Popen(
568
+ [openssl_path, "x509", "-in", cert_pem_filepath, "-noout", "-text"],
569
+ stdout=subprocess.PIPE,
570
+ stderr=subprocess.PIPE,
571
+ ) as proc:
572
+ data_bytes, err = proc.communicate()
573
+ if proc.returncode != 0:
574
+ raise IOError("Error loading {0}: {1}".format(cert_pem_filepath, err))
575
+ data_str = data_bytes.decode("utf8")
576
+ # init
577
+ subject_domain = None
578
+ san_domains = []
579
+ # regex!
580
+ _common_name = RE_openssl_x509_subject.search(data_str)
581
+ if _common_name is not None:
582
+ subject_domain = _common_name.group(1).lower()
583
+ san_domains = san_domains_from_text(data_str)
584
+ if subject_domain is not None and subject_domain not in san_domains:
585
+ san_domains.insert(0, subject_domain)
586
+ san_domains.sort()
587
+ return san_domains
588
+
589
+
590
+ def parse_csr_domains(
591
+ csr_pem: str,
592
+ csr_pem_filepath: Optional[str] = None,
593
+ submitted_domain_names: Optional[List[str]] = None,
594
+ ) -> List[str]:
595
+ """
596
+ checks found names against `submitted_domain_names`
597
+
598
+ This routine will use cryptography if available.
599
+ If not, openssl is used via subprocesses
600
+
601
+ `submitted_domain_names` should be all lowecase
602
+
603
+ :param csr_pem: a PEM encoded CSR, required
604
+ :type csr_pem: str
605
+ :param csr_pem_filepath: Optional filepath to the PEM encoded CSR.
606
+ Only used for commandline OpenSSL fallback operations.
607
+ :type csr_pem_filepath: str
608
+ :param submitted_domain_names: Optional. Default `None``. A list of fully
609
+ qualified domain names, all lowercase. If provided, parity between the
610
+ detected and submitted domains will be checked, and a `ValueError` will be
611
+ raised if the lists are not identical.
612
+ :type submitted_domain_names: list
613
+ :returns: List of Fully Qualified Domain Names (str) in the CSR
614
+ :rtype: list
615
+
616
+ The OpenSSL Equivalent / Fallback is::
617
+
618
+ openssl req -in {FILEPATH} -noout -text
619
+ """
620
+ log.info("parse_csr_domains >")
621
+ if conditionals.cryptography:
622
+ csr = conditionals.cryptography.x509.load_pem_x509_csr(csr_pem.encode())
623
+ found_domains = _cryptography__cert_or_req__all_names(csr)
624
+ else:
625
+ log.debug(".parse_csr_domains > openssl fallback")
626
+ # fallback onto OpenSSL
627
+ # openssl req -in MYCSR -noout -text
628
+ if not csr_pem_filepath:
629
+ # TODO: generate csr_pem_filepath if needed?
630
+ raise FallbackError_FilepathRequired("Must submit `csr_pem_filepath`.")
631
+ if openssl_version is None:
632
+ check_openssl_version()
633
+
634
+ with psutil.Popen(
635
+ [openssl_path, "req", "-in", csr_pem_filepath, "-noout", "-text"],
636
+ stdout=subprocess.PIPE,
637
+ stderr=subprocess.PIPE,
638
+ ) as proc:
639
+ data_bytes, err = proc.communicate()
640
+ if proc.returncode != 0:
641
+ raise IOError("Error loading {0}: {1}".format(csr_pem_filepath, err))
642
+ data_str = data_bytes.decode("utf8")
643
+
644
+ # parse the sans first, then add the commonname
645
+ found_domains = san_domains_from_text(data_str)
646
+
647
+ # note the conditional whitespace before/after CN
648
+ common_name = RE_openssl_x509_subject.search(data_str)
649
+ if common_name is not None:
650
+ found_domains.insert(0, common_name.group(1))
651
+
652
+ # ensure our CERT matches our submitted_domain_names
653
+ if submitted_domain_names is not None:
654
+ for domain in found_domains:
655
+ if domain not in submitted_domain_names:
656
+ raise ValueError("domain %s not in submitted_domain_names" % domain)
657
+ for domain in submitted_domain_names:
658
+ if domain not in found_domains:
659
+ raise ValueError("domain %s not in found_domains" % domain)
660
+
661
+ return sorted(found_domains)
662
+
663
+
664
+ def validate_key(
665
+ key_pem: str,
666
+ key_pem_filepath: Optional[str] = None,
667
+ ) -> Optional[TECHNOLOGY_RETURN_VALUES]:
668
+ """
669
+ raises an Exception if invalid
670
+ returns the key_technology if valid
671
+
672
+ This routine will use cryptography if available.
673
+ If not, openssl is used via subprocesses
674
+
675
+ This may have issues on older openssl systems
676
+
677
+ :param key_pem: a PEM encoded PrivateKey
678
+ :type key_pem: str
679
+ :param key_pem_filepath: Optional filepath to the PEM encoded PrivateKey.
680
+ Only used for commandline OpenSSL fallback operations.
681
+ :type key_pem_filepath: str
682
+ :returns: If the key is valid, it will return a Tuple wherein
683
+ - The first element is the Key's technology (EC, RSA)
684
+ - The second element is a Tuple with the key's type.
685
+ (RSA, (bits, ))
686
+ (EC, (curve_name, ))
687
+
688
+ If the key is not valid, an exception will be raised.
689
+ :rtype: str
690
+
691
+ The OpenSSL Equivalent / Fallback is::
692
+
693
+ openssl EC -in {FILEPATH}
694
+ openssl RSA -in {FILEPATH}
695
+
696
+ CHANGED
697
+ prior to v1.0.0, this only returned the key's technology (EC, RSA)
698
+
699
+ """
700
+ log.info("validate_key >")
701
+ if conditionals.cryptography:
702
+ if TYPE_CHECKING:
703
+ assert conditionals.crypto_serialization is not None
704
+ assert conditionals.crypto_rsa is not None
705
+ assert conditionals.crypto_ec is not None
706
+ try:
707
+ key = conditionals.crypto_serialization.load_pem_private_key(
708
+ key_pem.encode(), None
709
+ )
710
+ if isinstance(key, conditionals.crypto_rsa.RSAPrivateKey):
711
+ return ("RSA", (key.key_size,))
712
+ elif isinstance(key, conditionals.crypto_ec.EllipticCurvePrivateKey):
713
+ curve_name = curve_to_nist(key.curve.name)
714
+ return ("EC", (curve_name,))
715
+ return None
716
+ except Exception as exc:
717
+ raise OpenSslError_InvalidKey(exc)
718
+ log.debug(".validate_key > openssl fallback")
719
+ if not key_pem_filepath:
720
+ # TODO: generate a tempfile if needed?
721
+ raise FallbackError_FilepathRequired("Must submit `key_pem_filepath`.")
722
+ if openssl_version is None:
723
+ check_openssl_version()
724
+
725
+ def _check_fallback(_technology: Literal["rsa", "ec"]) -> Optional[Union[int, str]]:
726
+ log.debug(".validate_key > openssl fallback: _check_fallback[%s]", _technology)
727
+ # openssl rsa -in {KEY} -check
728
+ try:
729
+ with psutil.Popen(
730
+ [openssl_path, _technology, "-in", key_pem_filepath, "-noout", "-text"],
731
+ stdout=subprocess.PIPE,
732
+ stderr=subprocess.PIPE,
733
+ ) as proc:
734
+ data_bytes, err = proc.communicate()
735
+ if not data_bytes:
736
+ raise OpenSslError_InvalidKey(err)
737
+ data_str = data_bytes.decode("utf8")
738
+
739
+ if _technology == "rsa":
740
+ _rsa_pattern = r"Private-Key:\s+\((\d+) bit, 2 primes\)\s"
741
+ _matched = re.search(
742
+ _rsa_pattern, data_str, re.MULTILINE | re.DOTALL
743
+ )
744
+ if _matched:
745
+ _bits = int(_matched.groups()[0])
746
+ return _bits
747
+ raise OpenSslError_InvalidKey("trouble parsing")
748
+ elif _technology == "ec":
749
+ _ec_pattern_a = r"ANS1 OID:\s+([\w]+)\s"
750
+ _matched = re.search(
751
+ _ec_pattern_a, data_str, re.MULTILINE | re.DOTALL
752
+ )
753
+ if _matched:
754
+ _curve = _matched.groups()[0]
755
+ return _curve
756
+ _ec_pattern_b = r"NIST CURVE:\s+(P\-[\d]+)\s"
757
+ _matched = re.search(
758
+ _ec_pattern_b, data_str, re.MULTILINE | re.DOTALL
759
+ )
760
+ if _matched:
761
+ _curve = _matched.groups()[0]
762
+ return _curve
763
+ raise OpenSslError_InvalidKey("trouble parsing")
764
+
765
+ except OpenSslError_InvalidKey as exc: # noqa: F841
766
+ return None
767
+
768
+ _rsa_bits = _check_fallback("rsa")
769
+ if _rsa_bits:
770
+ return ("RSA", (_rsa_bits,))
771
+ _ec_curve = _check_fallback("ec")
772
+ if _ec_curve:
773
+ return ("EC", (_ec_curve,))
774
+
775
+ raise OpenSslError_InvalidKey()
776
+
777
+
778
+ def validate_csr(
779
+ csr_pem: str,
780
+ csr_pem_filepath: Optional[str] = None,
781
+ ) -> bool:
782
+ """
783
+ raises an error if invalid
784
+
785
+ This routine will use cryptography if available.
786
+ If not, openssl is used via subprocesses
787
+
788
+ :param csr_pem: a PEM encoded CSR, required
789
+ :type csr_pem: str
790
+ :param csr_pem_filepath: Optional filepath to the PEM encoded CSR.
791
+ Only used for commandline OpenSSL fallback operations.
792
+ :type csr_pem_filepath: str
793
+ :returns: True
794
+ :rtype: bool
795
+
796
+ The OpenSSL Equivalent / Fallback is::
797
+
798
+ openssl req -text -noout -verify -in {FILEPATH}
799
+ """
800
+ log.info("validate_csr >")
801
+ if conditionals.cryptography:
802
+ csr = conditionals.cryptography.x509.load_pem_x509_csr(csr_pem.encode())
803
+ if not csr.is_signature_valid:
804
+ raise OpenSslError_InvalidCSR("invalid CSR signature")
805
+ return True
806
+
807
+ log.debug(".validate_csr > openssl fallback")
808
+ # openssl req -text -noout -verify -in {CSR}
809
+ if not csr_pem_filepath:
810
+ # TODO: generate tempfile if needed?
811
+ raise FallbackError_FilepathRequired("Must submit `csr_pem_filepath`.")
812
+ if openssl_version is None:
813
+ check_openssl_version()
814
+
815
+ with psutil.Popen(
816
+ [openssl_path, "req", "-text", "-noout", "-verify", "-in", csr_pem_filepath],
817
+ stdout=subprocess.PIPE,
818
+ stderr=subprocess.PIPE,
819
+ ) as proc:
820
+ data_bytes, err = proc.communicate()
821
+ if proc.returncode or not data_bytes or b"verify failure" in err.lower():
822
+ raise OpenSslError_InvalidCSR(err)
823
+ # this may be True or bytes, depending on the version
824
+ # in any event, being here means we passed
825
+ return True
826
+
827
+
828
+ def validate_cert(
829
+ cert_pem: str,
830
+ cert_pem_filepath: Optional[str] = None,
831
+ ) -> bool:
832
+ """
833
+ raises an error if invalid
834
+
835
+ This routine will use cryptography if available.
836
+ If not, openssl is used via subprocesses
837
+
838
+ :param cert_pem: a PEM encoded Certificate
839
+ :type cert_pem: str
840
+ :param cert_pem_filepath: Optional filepath to the PEM encoded Certificate.
841
+ Only used for commandline OpenSSL fallback operations.
842
+ :type cert_pem_filepath: str
843
+ :returns: True
844
+ :rtype: bool
845
+
846
+ The OpenSSL Equivalent / Fallback is::
847
+
848
+ openssl x509 -in {FILEPATH} -inform PEM -noout -text
849
+ """
850
+ log.info("validate_cert >")
851
+ if conditionals.cryptography:
852
+ try:
853
+ cert = conditionals.cryptography.x509.load_pem_x509_certificate(
854
+ cert_pem.encode()
855
+ )
856
+ except Exception as exc:
857
+ raise OpenSslError_InvalidCertificate(exc)
858
+ if not cert:
859
+ raise OpenSslError_InvalidCertificate()
860
+ return True
861
+
862
+ log.debug(".validate_cert > openssl fallback")
863
+ if openssl_version is None:
864
+ check_openssl_version()
865
+
866
+ # generate `cert_pem_filepath` if needed.
867
+ _tmpfile_cert = None
868
+ if not cert_pem_filepath:
869
+ _tmpfile_cert = new_pem_tempfile(cert_pem)
870
+ cert_pem_filepath = _tmpfile_cert.name
871
+ try:
872
+ # openssl x509 -in {CERTIFICATE} -inform pem -noout -text
873
+ with psutil.Popen(
874
+ [
875
+ openssl_path,
876
+ "x509",
877
+ "-in",
878
+ cert_pem_filepath,
879
+ "-inform",
880
+ "PEM",
881
+ "-noout",
882
+ "-text",
883
+ ],
884
+ stdout=subprocess.PIPE,
885
+ stderr=subprocess.PIPE,
886
+ ) as proc:
887
+ data_bytes, err = proc.communicate()
888
+ if not data_bytes:
889
+ raise OpenSslError_InvalidCertificate(err)
890
+ # this may be True or bytes, depending on the version
891
+ # in any event, being here means we passed
892
+ finally:
893
+ if _tmpfile_cert:
894
+ _tmpfile_cert.close()
895
+ return True
896
+
897
+
898
+ def fingerprint_cert(
899
+ cert_pem: str,
900
+ cert_pem_filepath: Optional[str] = None,
901
+ algorithm: str = "sha1",
902
+ ) -> str:
903
+ """
904
+ Derives the Certificate's fingerprint
905
+
906
+ This routine will use cryptography if available.
907
+ If not, openssl is used via subprocesses
908
+
909
+ colons will be removed, they can be reintroduced on render
910
+
911
+ Every openssl version tested so-far defaults to sha1
912
+
913
+ openssl x509 -noout -fingerprint -inform pem -in isrgrootx1.pem
914
+ SHA1 Fingerprint=CA:BD:2A:79:A1:07:6A:31:F2:1D:25:36:35:CB:03:9D:43:29:A5:E8
915
+
916
+ openssl x509 -noout -fingerprint -sha1 -inform pem -in isrgrootx1.pem
917
+ SHA1 Fingerprint=CA:BD:2A:79:A1:07:6A:31:F2:1D:25:36:35:CB:03:9D:43:29:A5:E8
918
+
919
+ openssl x509 -noout -fingerprint -md5 -inform pem -in isrgrootx1.pem
920
+ MD5 Fingerprint=0C:D2:F9:E0:DA:17:73:E9:ED:86:4D:A5:E3:70:E7:4E
921
+
922
+ openssl x509 -noout -fingerprint -sha256 -inform pem -in isrgrootx1.pem
923
+ SHA256 Fingerprint=96:BC:EC:06:26:49:76:F3:74:60:77:9A:CF:28:C5:A7:CF:E8:A3:C0:AA:E1:1A:8F:FC:EE:05:C0:BD:DF:08:C6
924
+
925
+ :param cert_pem: a PEM encoded Certificate
926
+ :type cert_pem: str
927
+ :param cert_pem_filepath: Optional filepath to the PEM encoded Certificate.
928
+ Only used for commandline OpenSSL fallback operations.
929
+ :type cert_pem_filepath: str
930
+ :param algorithm: default "sha1"
931
+ :type algorithm: str
932
+ :returns: Raw fingerprint data (e.g. without notation to separate pairs with colons)
933
+ :rtype: str
934
+
935
+ The OpenSSL Equivalent / Fallback is::
936
+
937
+ openssl x509 -noout -fingerprint -{algorithm} -inform PEM -in {CERTIFICATE}
938
+ """
939
+ log.info("fingerprint_cert >")
940
+ _accepted_algorithms = ("sha1", "sha256", "md5")
941
+ if algorithm not in _accepted_algorithms:
942
+ raise ValueError(
943
+ "algorithm `%s` not in `%s`" % (algorithm, _accepted_algorithms)
944
+ )
945
+ if conditionals.cryptography:
946
+ if TYPE_CHECKING:
947
+ assert conditionals.crypto_hashes is not None
948
+ try:
949
+ cert = conditionals.cryptography.x509.load_pem_x509_certificate(
950
+ cert_pem.encode()
951
+ )
952
+ except Exception as exc:
953
+ raise OpenSslError_InvalidCertificate(exc)
954
+ if not cert:
955
+ raise OpenSslError_InvalidCertificate()
956
+ _hash: "HashAlgorithm"
957
+ if algorithm == "sha1":
958
+ _hash = conditionals.crypto_hashes.SHA1()
959
+ elif algorithm == "sha256":
960
+ _hash = conditionals.crypto_hashes.SHA256()
961
+ elif algorithm == "md5":
962
+ _hash = conditionals.crypto_hashes.MD5()
963
+ _fingerprint_bytes = cert.fingerprint(_hash)
964
+ fingerprint = _fingerprint_bytes.hex().upper()
965
+ return fingerprint
966
+
967
+ log.debug(".fingerprint_cert > openssl fallback")
968
+ if openssl_version is None:
969
+ check_openssl_version()
970
+
971
+ # generate tempfile if needed
972
+ _tmpfile_cert = None
973
+ if not cert_pem_filepath:
974
+ _tmpfile_cert = new_pem_tempfile(cert_pem)
975
+ cert_pem_filepath = _tmpfile_cert.name
976
+ try:
977
+ with psutil.Popen(
978
+ [
979
+ openssl_path,
980
+ "x509",
981
+ "-noout",
982
+ "-fingerprint",
983
+ "-%s" % algorithm,
984
+ "-inform",
985
+ "PEM",
986
+ "-in",
987
+ cert_pem_filepath,
988
+ ],
989
+ stdout=subprocess.PIPE,
990
+ stderr=subprocess.PIPE,
991
+ ) as proc:
992
+ data_bytes, err = proc.communicate()
993
+ if not data_bytes:
994
+ raise OpenSslError_InvalidCertificate(err)
995
+ data_str = data_bytes.decode("utf8")
996
+
997
+ # the output will look something like this:
998
+ # 'SHA1 Fingerprint=F6:3C:5C:66:B5:25:51:EE:DA:DF:7C:E4:43:01:D6:46:68:0B:8F:5D\n'
999
+ data_str = data_str.strip().split("=")[1]
1000
+ data_str = data_str.replace(":", "")
1001
+ finally:
1002
+ if _tmpfile_cert:
1003
+ _tmpfile_cert.close()
1004
+ return data_str
1005
+
1006
+
1007
+ def modulus_md5_key(
1008
+ key_pem: str,
1009
+ key_pem_filepath: Optional[str] = None,
1010
+ ) -> Optional[str]:
1011
+ """
1012
+ This routine will use cryptography if available.
1013
+ If not, openssl is used via subprocesses
1014
+
1015
+ :param key_pem: a PEM encoded PrivateKey
1016
+ :type key_pem: str
1017
+ :param key_pem_filepath: Optional filepath to the PEM encoded PrivateKey.
1018
+ Only used for commandline OpenSSL fallback operations.
1019
+ :type key_pem_filepath: str
1020
+ :returns: md5 digest of key's modulus
1021
+ :rtype: str or None
1022
+
1023
+ The OpenSSL Equivalent / Fallback is::
1024
+
1025
+ md5(openssl rsa -noout -modulus -in {FILEPATH})
1026
+ """
1027
+ # ???: Should this raise an Exception instead of returning `None`?
1028
+ log.info("modulus_md5_key >")
1029
+ if conditionals.cryptography:
1030
+ if TYPE_CHECKING:
1031
+ assert conditionals.crypto_rsa is not None
1032
+ assert conditionals.crypto_serialization is not None
1033
+ privkey = conditionals.crypto_serialization.load_pem_private_key(
1034
+ key_pem.encode(), password=None
1035
+ )
1036
+ if isinstance(privkey, conditionals.crypto_rsa.RSAPrivateKey):
1037
+ modn = privkey.public_key().public_numbers().n # type: ignore[union-attr]
1038
+ data_str = "{:X}".format(modn)
1039
+ else:
1040
+ return None
1041
+ else:
1042
+ log.debug(".modulus_md5_key > openssl fallback")
1043
+ if not key_pem_filepath:
1044
+ # TODO: generate if needed?
1045
+ raise FallbackError_FilepathRequired("Must submit `key_pem_filepath`.")
1046
+ if openssl_version is None:
1047
+ check_openssl_version()
1048
+
1049
+ # original code was:
1050
+ # openssl rsa -noout -modulus -in {KEY} | openssl md5
1051
+ # BUT
1052
+ # that pipes into md5: "Modulus={MOD}\n"
1053
+ with psutil.Popen(
1054
+ [openssl_path, "rsa", "-noout", "-modulus", "-in", key_pem_filepath],
1055
+ stdout=subprocess.PIPE,
1056
+ stderr=subprocess.PIPE,
1057
+ ) as proc_modulus:
1058
+ data_bytes, err = proc_modulus.communicate()
1059
+ data_str = data_bytes.decode("utf8")
1060
+ data_str = _cleanup_openssl_modulus(data_str)
1061
+ if not data_str:
1062
+ return None
1063
+ data_bytes = data_str.encode()
1064
+ data_str = hashlib.md5(data_bytes).hexdigest()
1065
+ return data_str
1066
+
1067
+
1068
+ def modulus_md5_csr(
1069
+ csr_pem: str,
1070
+ csr_pem_filepath: Optional[str] = None,
1071
+ ) -> Optional[str]:
1072
+ """
1073
+ This routine will use cryptography if available.
1074
+ If not, openssl is used via subprocesses
1075
+
1076
+ :param csr_pem: a PEM encoded CSR
1077
+ :type csr_pem: str
1078
+ :param csr_pem_filepath: Optional filepath to the PEM encoded CSR.
1079
+ Only used for commandline OpenSSL fallback operations.
1080
+ :type csr_pem_filepath: str
1081
+ :returns: md5 digest of CSR's modulus
1082
+ :rtype: str or None
1083
+
1084
+ The OpenSSL Equivalent / Fallback is::
1085
+
1086
+ md5(openssl req -noout -modulus -in {FILEPATH})
1087
+ """
1088
+ # TODO: Support EC Key Modulus Variant - https://github.com/aptise/cert_utils/issues/15
1089
+ # ???: Should this raise an Exception instead of returning `None`?
1090
+ log.info("modulus_md5_csr >")
1091
+ if conditionals.cryptography:
1092
+ csr = conditionals.cryptography.x509.load_pem_x509_csr(csr_pem.encode())
1093
+ _pubkey = csr.public_key()
1094
+ _keytype = _cryptography__public_key_technology(_pubkey)
1095
+ assert _keytype
1096
+ _keytype_basic = _keytype[0]
1097
+ if _keytype_basic == "RSA":
1098
+ modn = _pubkey.public_numbers().n # type: ignore[union-attr]
1099
+ data_str = "{:X}".format(modn)
1100
+ elif _keytype_basic == "EC":
1101
+ return None
1102
+ else:
1103
+ return None
1104
+ else:
1105
+ log.debug(".modulus_md5_csr > openssl fallback")
1106
+ # original code was:
1107
+ # openssl req -noout -modulus -in {CSR} | openssl md5
1108
+ # BUT
1109
+ # that pipes into md5: "Modulus={MOD}\n"
1110
+ if not csr_pem_filepath:
1111
+ # TODO: generate if needed?
1112
+ raise FallbackError_FilepathRequired("Must submit `csr_pem_filepath`.")
1113
+ if openssl_version is None:
1114
+ check_openssl_version()
1115
+
1116
+ with psutil.Popen(
1117
+ [openssl_path, "req", "-noout", "-modulus", "-in", csr_pem_filepath],
1118
+ stdout=subprocess.PIPE,
1119
+ stderr=subprocess.PIPE,
1120
+ ) as proc_modulus:
1121
+ data_bytes, err = proc_modulus.communicate()
1122
+ data_str = data_bytes.decode("utf8")
1123
+ data_str = _cleanup_openssl_modulus(data_str)
1124
+ if not data_str:
1125
+ return None
1126
+ data_bytes = data_str.encode()
1127
+ data_str = hashlib.md5(data_bytes).hexdigest()
1128
+ return data_str
1129
+
1130
+
1131
+ def modulus_md5_cert(
1132
+ cert_pem: str,
1133
+ cert_pem_filepath: Optional[str] = None,
1134
+ ) -> Optional[str]:
1135
+ """
1136
+ This routine will use cryptography if available.
1137
+ If not, openssl is used via subprocesses
1138
+
1139
+ :param cert_pem: a PEM encoded Certificate
1140
+ :type cert_pem: str
1141
+ :param cert_pem_filepath: Optional filepath to the PEM encoded Certificate.
1142
+ Only used for commandline OpenSSL fallback operations.
1143
+ :type cert_pem_filepath: str
1144
+ :returns: md5 digest of Certificate's modulus
1145
+ :rtype: str or None
1146
+
1147
+ The OpenSSL Equivalent / Fallback is::
1148
+
1149
+ md5(openssl x509 -noout -modulus -in {FILEPATH})
1150
+ """
1151
+ log.info("modulus_md5_cert >")
1152
+ if conditionals.cryptography:
1153
+ cert = conditionals.cryptography.x509.load_pem_x509_certificate(
1154
+ cert_pem.encode()
1155
+ )
1156
+ _pubkey = cert.public_key()
1157
+ _keytype = _cryptography__public_key_technology(_pubkey)
1158
+ assert _keytype
1159
+ _keytype_basic = _keytype[0]
1160
+ if _keytype_basic == "RSA":
1161
+ modn = _pubkey.public_numbers().n # type: ignore[union-attr]
1162
+ data_str = "{:X}".format(modn)
1163
+ else:
1164
+ return None
1165
+ else:
1166
+ log.debug(".modulus_md5_cert > openssl fallback")
1167
+ # original code was:
1168
+ # openssl x509 -noout -modulus -in {CERT} | openssl md5
1169
+ # BUT
1170
+ # that pipes into md5: "Modulus={MOD}\n"
1171
+ if not cert_pem_filepath:
1172
+ # TODO: generate if needed?
1173
+ raise FallbackError_FilepathRequired("Must submit `cert_pem_filepath`.")
1174
+ if openssl_version is None:
1175
+ check_openssl_version()
1176
+
1177
+ with psutil.Popen(
1178
+ [openssl_path, "x509", "-noout", "-modulus", "-in", cert_pem_filepath],
1179
+ stdout=subprocess.PIPE,
1180
+ stderr=subprocess.PIPE,
1181
+ ) as proc_modulus:
1182
+ data_bytes, err = proc_modulus.communicate()
1183
+ data_str = data_bytes.decode("utf8")
1184
+ data_str = _cleanup_openssl_modulus(data_str)
1185
+ if "Wrong Algorithm type" in data_str:
1186
+ # openssl 1.1.x
1187
+ return None
1188
+ if "No modulus for this public key type" in data_str:
1189
+ # openssl 3.0.x
1190
+ return None
1191
+ data_bytes = data_str.encode()
1192
+ data_str = hashlib.md5(data_bytes).hexdigest()
1193
+ return data_str
1194
+
1195
+
1196
+ def parse_cert__enddate(
1197
+ cert_pem: str,
1198
+ cert_pem_filepath: Optional[str] = None,
1199
+ ) -> "datetime.datetime":
1200
+ """
1201
+ This routine will use cryptography if available.
1202
+ If not, openssl is used via subprocesses
1203
+
1204
+ :param cert_pem: PEM encoded Certificate
1205
+ :type cert_pem: str
1206
+ :param cert_pem_filepath: Optional filepath to the PEM encoded Certificate.
1207
+ Only used for commandline OpenSSL fallback operations.
1208
+ :type cert_pem_filepath: str
1209
+ :returns: end date
1210
+ :rtype: datetime.datetime
1211
+
1212
+ The OpenSSL Equivalent / Fallback is::
1213
+
1214
+ openssl x509 -noout -enddate -in {FILEPATH})
1215
+ """
1216
+ log.info("parse_cert__enddate >")
1217
+ if conditionals.cryptography:
1218
+ cert = conditionals.cryptography.x509.load_pem_x509_certificate(
1219
+ cert_pem.encode()
1220
+ )
1221
+ date = cert.not_valid_after_utc
1222
+ else:
1223
+ log.debug(".parse_cert__enddate > openssl fallback")
1224
+ # openssl x509 -enddate -noout -in {CERT}
1225
+ if not cert_pem_filepath:
1226
+ raise FallbackError_FilepathRequired("Must submit `cert_pem_filepath`.")
1227
+ data = _openssl_cert_single_op__pem_filepath(cert_pem_filepath, "-enddate")
1228
+ if data[:9] != "notAfter=":
1229
+ raise OpenSslError_InvalidCertificate("unexpected format")
1230
+ data_date = data[9:]
1231
+ date = dateutil_parser.parse(data_date)
1232
+ # date = date.replace(tzinfo=None)
1233
+ return date
1234
+
1235
+
1236
+ def parse_cert__startdate(
1237
+ cert_pem: str,
1238
+ cert_pem_filepath: Optional[str] = None,
1239
+ ) -> "datetime.datetime":
1240
+ """
1241
+ This routine will use cryptography if available.
1242
+ If not, openssl is used via subprocesses
1243
+
1244
+ :param cert_pem: PEM encoded Certificate
1245
+ :type cert_pem: str
1246
+ :param cert_pem_filepath: Optional filepath to the PEM encoded Certificate.
1247
+ Only used for commandline OpenSSL fallback operations.
1248
+ :type cert_pem_filepath: str
1249
+ :returns: start date
1250
+ :rtype: datetime.datetime
1251
+
1252
+ The OpenSSL Equivalent / Fallback is::
1253
+
1254
+ openssl x509 -noout -startdate -in {FILEPATH})
1255
+ """
1256
+ log.info("parse_cert__startdate >")
1257
+ if conditionals.cryptography:
1258
+ cert = conditionals.cryptography.x509.load_pem_x509_certificate(
1259
+ cert_pem.encode()
1260
+ )
1261
+ date = cert.not_valid_before_utc
1262
+ else:
1263
+ log.debug(".parse_cert__startdate > openssl fallback")
1264
+ # openssl x509 -startdate -noout -in {CERT}
1265
+ if not cert_pem_filepath:
1266
+ raise FallbackError_FilepathRequired("Must submit `cert_pem_filepath`.")
1267
+ data = _openssl_cert_single_op__pem_filepath(cert_pem_filepath, "-startdate")
1268
+ if data[:10] != "notBefore=":
1269
+ raise OpenSslError_InvalidCertificate("unexpected format")
1270
+ data_date = data[10:]
1271
+ date = dateutil_parser.parse(data_date)
1272
+ # date = date.replace(tzinfo=None)
1273
+ return date
1274
+
1275
+
1276
+ def parse_cert__spki_sha256(
1277
+ cert_pem: str,
1278
+ cert_pem_filepath: Optional[str] = None,
1279
+ cryptography_cert: Optional["Certificate"] = None,
1280
+ key_technology_basic: Optional[str] = None,
1281
+ as_b64: Optional[bool] = None,
1282
+ ) -> str:
1283
+ """
1284
+ :param str cert_pem: PEM encoded Certificate
1285
+ :param str cert_pem_filepath: Optional filepath to PEM encoded Certificate.
1286
+ Only used for commandline OpenSSL fallback operations.
1287
+ :param cryptography_cert: optional hint to aid in crypto commands
1288
+ :type cryptography_cert: `OpenSSL.crypto.load_certificate(...).to_cryptography()``
1289
+ :param str key_technology_basic: optional hint to aid in openssl fallback
1290
+ :param bool as_b64: encode with b64?
1291
+ :returns: spki sha256
1292
+ :rtype: str
1293
+
1294
+ The OpenSSL Equivalent / Fallback is::
1295
+
1296
+ :function :_openssl_spki_hash_cert
1297
+ """
1298
+ log.info("parse_cert__spki_sha256 >")
1299
+ if conditionals.cryptography:
1300
+ if not cryptography_cert:
1301
+ cryptography_cert = (
1302
+ conditionals.cryptography.x509.load_pem_x509_certificate(
1303
+ cert_pem.encode()
1304
+ )
1305
+ )
1306
+ assert cryptography_cert is not None # nest under `if TYPE_CHECKING` not needed
1307
+ cryptography_publickey = cryptography_cert.public_key()
1308
+ return _cryptography__public_key_spki_sha256(
1309
+ cryptography_publickey,
1310
+ as_b64=as_b64,
1311
+ )
1312
+ log.debug(".parse_cert__spki_sha256 > openssl fallback")
1313
+ if not cert_pem_filepath:
1314
+ # TODO: generate tempfile?
1315
+ raise FallbackError_FilepathRequired("Must submit `cert_pem_filepath`.")
1316
+ tmpfile_pem = None
1317
+ try:
1318
+ if key_technology_basic is None:
1319
+ key_technology = parse_cert__key_technology(
1320
+ cert_pem=cert_pem, cert_pem_filepath=cert_pem_filepath
1321
+ )
1322
+ if not key_technology:
1323
+ raise ValueError("Could not parse key_technology for backup")
1324
+ key_technology_basic = key_technology[0]
1325
+ spki_sha256 = _openssl_spki_hash_cert(
1326
+ key_technology_basic=key_technology_basic,
1327
+ cert_pem_filepath=cert_pem_filepath,
1328
+ as_b64=as_b64,
1329
+ )
1330
+ return spki_sha256
1331
+ except Exception as exc: # noqa: F841
1332
+ raise
1333
+ finally:
1334
+ if tmpfile_pem:
1335
+ tmpfile_pem.close()
1336
+
1337
+
1338
+ def parse_cert__key_technology(
1339
+ cert_pem: str,
1340
+ cert_pem_filepath: Optional[str] = None,
1341
+ ) -> Optional[TECHNOLOGY_RETURN_VALUES]:
1342
+ """
1343
+ :param cert_pem: PEM encoded Certificate
1344
+ :type cert_pem: str
1345
+ :param cert_pem_filepath: Optional filepath to PEM encoded Certificate.
1346
+ Only used for commandline OpenSSL fallback operations.
1347
+ :type cert_pem_filepath: str
1348
+ :returns: key technology type
1349
+ :rtype: TECHNOLOGY_RETURN_VALUES
1350
+
1351
+ The OpenSSL Equivalent / Fallback is::
1352
+ Regex the output of::
1353
+
1354
+ openssl x509 -in {FILEPATH} -noout -text
1355
+ """
1356
+ log.info("parse_cert__key_technology >")
1357
+ if conditionals.cryptography:
1358
+ cert = conditionals.cryptography.x509.load_pem_x509_certificate(
1359
+ cert_pem.encode()
1360
+ )
1361
+ publickey = cert.public_key()
1362
+ return _cryptography__public_key_technology(publickey)
1363
+ log.debug(".parse_cert__key_technology > openssl fallback")
1364
+ # `openssl x509 -in MYCERT -noout -text`
1365
+ if openssl_version is None:
1366
+ check_openssl_version()
1367
+
1368
+ if not cert_pem_filepath:
1369
+ # TODO: generate tempfile?
1370
+ raise FallbackError_FilepathRequired("Must submit `cert_pem_filepath`.")
1371
+ with psutil.Popen(
1372
+ [openssl_path, "x509", "-in", cert_pem_filepath, "-noout", "-text"],
1373
+ stdout=subprocess.PIPE,
1374
+ stderr=subprocess.PIPE,
1375
+ ) as proc:
1376
+ _data_bytes, err = proc.communicate()
1377
+ if proc.returncode != 0:
1378
+ raise IOError("Error loading {0}: {1}".format(cert_pem_filepath, err))
1379
+ data_str = _data_bytes.decode("utf8")
1380
+ return _cert_pubkey_technology__text(data_str)
1381
+
1382
+
1383
+ def parse_cert(
1384
+ cert_pem: str,
1385
+ cert_pem_filepath: Optional[str] = None,
1386
+ ) -> CertificateData:
1387
+ """
1388
+ This routine will use cryptography if available.
1389
+ If not, openssl is used via subprocesses
1390
+
1391
+ :param cert_pem: PEM encoded Certificate
1392
+ :type cert_pem: str
1393
+ :param cert_pem_filepath: Optional filepath to PEM encoded Certificate
1394
+ Only used for commandline OpenSSL fallback operations.
1395
+ :type cert_pem_filepath: str
1396
+ :returns: dict representation of select Certificate information
1397
+ :rtype: dict
1398
+ """
1399
+ log.info("parse_cert >")
1400
+ rval: CertificateData = {
1401
+ "issuer": None,
1402
+ "subject": None,
1403
+ "enddate": None,
1404
+ "startdate": None,
1405
+ "SubjectAlternativeName": None,
1406
+ "key_technology": None,
1407
+ "fingerprint_sha1": None,
1408
+ "spki_sha256": None,
1409
+ "issuer_uri": None,
1410
+ "authority_key_identifier": None,
1411
+ "serial": None,
1412
+ }
1413
+
1414
+ if conditionals.cryptography:
1415
+ if TYPE_CHECKING:
1416
+ assert conditionals.crypto_hashes is not None
1417
+ cert = conditionals.cryptography.x509.load_pem_x509_certificate(
1418
+ cert_pem.encode()
1419
+ )
1420
+ publickey = cert.public_key()
1421
+ rval["issuer"] = _format_cryptography_components(cert.issuer)
1422
+ rval["subject"] = _format_cryptography_components(cert.subject)
1423
+ rval["enddate"] = cert.not_valid_after_utc
1424
+ rval["startdate"] = cert.not_valid_before_utc
1425
+ rval["key_technology"] = _cryptography__public_key_technology(publickey)
1426
+ _fingerprint_bytes = cert.fingerprint(conditionals.crypto_hashes.SHA1())
1427
+ rval["fingerprint_sha1"] = _fingerprint_bytes.hex().upper()
1428
+ rval["spki_sha256"] = parse_cert__spki_sha256(
1429
+ cert_pem=cert_pem,
1430
+ cert_pem_filepath=cert_pem_filepath,
1431
+ cryptography_cert=cert,
1432
+ as_b64=False,
1433
+ )
1434
+ rval["serial"] = cert.serial_number
1435
+ try:
1436
+ ext = cert.extensions.get_extension_for_oid(
1437
+ conditionals.cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
1438
+ )
1439
+ if ext:
1440
+ _names: List[str] = ext.value.get_values_for_type(conditionals.cryptography.x509.DNSName) # type: ignore[attr-defined]
1441
+ rval["SubjectAlternativeName"] = sorted(_names)
1442
+ except conditionals.cryptography.x509.ExtensionNotFound as exc: # noqa: F841
1443
+ pass
1444
+ try:
1445
+ ext = cert.extensions.get_extension_for_oid(
1446
+ conditionals.cryptography.x509.oid.ExtensionOID.AUTHORITY_KEY_IDENTIFIER
1447
+ )
1448
+ if ext:
1449
+ # this comes out as binary, so we need to convert it to the
1450
+ # openssl version, which is an list of uppercase hex pairs
1451
+ _as_binary = ext.value.key_identifier # type: ignore[attr-defined]
1452
+ rval["authority_key_identifier"] = convert_binary_to_hex(_as_binary)
1453
+ except conditionals.cryptography.x509.ExtensionNotFound as exc: # noqa: F841
1454
+ pass
1455
+ try:
1456
+ ext = cert.extensions.get_extension_for_oid(
1457
+ conditionals.cryptography.x509.oid.ExtensionOID.AUTHORITY_KEY_IDENTIFIER
1458
+ )
1459
+ if ext:
1460
+ # this comes out as binary, so we need to convert it to the
1461
+ # openssl version, which is an list of uppercase hex pairs
1462
+ _as_binary = ext.value.key_identifier # type: ignore[attr-defined]
1463
+ rval["authority_key_identifier"] = convert_binary_to_hex(_as_binary)
1464
+ except conditionals.cryptography.x509.ExtensionNotFound as exc: # noqa: F841
1465
+ pass
1466
+ try:
1467
+ ext = cert.extensions.get_extension_for_oid(
1468
+ conditionals.cryptography.x509.oid.ExtensionOID.AUTHORITY_INFORMATION_ACCESS
1469
+ )
1470
+ if ext:
1471
+ for _item in ext.value: # type: ignore[attr-defined]
1472
+ if not isinstance(
1473
+ _item,
1474
+ conditionals.cryptography.x509.extensions.AccessDescription,
1475
+ ):
1476
+ continue
1477
+ # _item.access_method is either:
1478
+ # * cryptography.x509.oid.AuthorityInformationAccessOID.OCSP
1479
+ # * cryptography.x509.oid.AuthorityInformationAccessOID.CA_ISSUERS
1480
+ # we only care about CA_ISSUERS
1481
+ if (
1482
+ _item.access_method
1483
+ == conditionals.cryptography.x509.oid.AuthorityInformationAccessOID.CA_ISSUERS
1484
+ ):
1485
+ if isinstance(
1486
+ _item.access_location,
1487
+ conditionals.cryptography.x509.UniformResourceIdentifier,
1488
+ ):
1489
+ rval["issuer_uri"] = _item.access_location.value
1490
+ except conditionals.cryptography.x509.ExtensionNotFound as exc: # noqa: F841
1491
+ pass
1492
+ return rval
1493
+
1494
+ log.debug(".parse_cert > openssl fallback")
1495
+ tmpfile_pem = None
1496
+ try:
1497
+ if not cert_pem_filepath:
1498
+ tmpfile_pem = new_pem_tempfile(cert_pem)
1499
+ cert_pem_filepath = tmpfile_pem.name
1500
+
1501
+ _issuer_b = _openssl_cert_single_op__pem_filepath(cert_pem_filepath, "-issuer")
1502
+ _subject_b = _openssl_cert_single_op__pem_filepath(
1503
+ cert_pem_filepath, "-subject"
1504
+ )
1505
+ rval["issuer"] = _format_openssl_components(_issuer_b, fieldset="issuer")
1506
+ rval["subject"] = _format_openssl_components(_subject_b, fieldset="subject")
1507
+ rval["startdate"] = parse_cert__startdate(
1508
+ cert_pem=cert_pem, cert_pem_filepath=cert_pem_filepath
1509
+ )
1510
+ rval["enddate"] = parse_cert__enddate(
1511
+ cert_pem=cert_pem, cert_pem_filepath=cert_pem_filepath
1512
+ )
1513
+ rval["key_technology"] = _key_technology = parse_cert__key_technology(
1514
+ cert_pem=cert_pem, cert_pem_filepath=cert_pem_filepath
1515
+ )
1516
+ rval["fingerprint_sha1"] = fingerprint_cert(
1517
+ cert_pem=cert_pem, cert_pem_filepath=cert_pem_filepath, algorithm="sha1"
1518
+ )
1519
+ if _key_technology:
1520
+ rval["spki_sha256"] = parse_cert__spki_sha256(
1521
+ cert_pem=cert_pem,
1522
+ cert_pem_filepath=cert_pem_filepath,
1523
+ key_technology_basic=_key_technology[0],
1524
+ as_b64=False,
1525
+ )
1526
+ try:
1527
+ _text = cert_ext__pem_filepath(cert_pem_filepath, "serial")
1528
+ serial_no = serial_from_text(_text)
1529
+ rval["serial"] = serial_no
1530
+ except Exception as exc: # noqa: F841
1531
+ pass
1532
+
1533
+ if openssl_version is None:
1534
+ check_openssl_version()
1535
+
1536
+ if _openssl_behavior == "b":
1537
+ try:
1538
+ _text = cert_ext__pem_filepath(cert_pem_filepath, "subjectAltName")
1539
+ found_domains = san_domains_from_text(_text)
1540
+ rval["SubjectAlternativeName"] = found_domains
1541
+ except Exception as exc: # noqa: F841
1542
+ pass
1543
+ try:
1544
+ _text = cert_ext__pem_filepath(
1545
+ cert_pem_filepath, "authorityKeyIdentifier"
1546
+ )
1547
+ authority_key_identifier = authority_key_identifier_from_text(_text)
1548
+ rval["authority_key_identifier"] = authority_key_identifier
1549
+ except Exception as exc: # noqa: F841
1550
+ pass
1551
+ try:
1552
+ _text = cert_ext__pem_filepath(cert_pem_filepath, "authorityInfoAccess")
1553
+ issuer_uri = issuer_uri_from_text(_text)
1554
+ rval["issuer_uri"] = issuer_uri
1555
+ except Exception as exc: # noqa: F841
1556
+ pass
1557
+ else:
1558
+ if openssl_version is None:
1559
+ check_openssl_version()
1560
+
1561
+ with psutil.Popen(
1562
+ [openssl_path, "x509", "-text", "-noout", "-in", cert_pem_filepath],
1563
+ stdout=subprocess.PIPE,
1564
+ stderr=subprocess.PIPE,
1565
+ ) as proc_text:
1566
+ data, err = proc_text.communicate()
1567
+ data = data.decode("utf8")
1568
+ found_domains = san_domains_from_text(data)
1569
+ rval["SubjectAlternativeName"] = found_domains
1570
+
1571
+ authority_key_identifier = authority_key_identifier_from_text(data)
1572
+ rval["authority_key_identifier"] = authority_key_identifier
1573
+
1574
+ issuer_uri = issuer_uri_from_text(data)
1575
+ rval["issuer_uri"] = issuer_uri
1576
+
1577
+ return rval
1578
+ except Exception as exc: # noqa: F841
1579
+ raise
1580
+ finally:
1581
+ if tmpfile_pem:
1582
+ tmpfile_pem.close()
1583
+
1584
+
1585
+ def parse_csr__key_technology(
1586
+ csr_pem: str,
1587
+ csr_pem_filepath: Optional[str] = None,
1588
+ csr: Optional["CertificateSigningRequest"] = None,
1589
+ ) -> Optional[TECHNOLOGY_RETURN_VALUES]:
1590
+ """
1591
+ :param csr_pem: PEM encoded CSR
1592
+ :type csr_pem: str
1593
+ :param csr_pem_filepath: Optional filepath to PEM encoded CSR.
1594
+ Only used for commandline OpenSSL fallback operations.
1595
+ :type csr_pem_filepath: str
1596
+ :param _csr: cryptography CSR object
1597
+ :type csr: `cryptography.x509.CertificateSigningRequest`
1598
+ :returns: key technology type
1599
+ :rtype: str or None
1600
+
1601
+ The OpenSSL Equivalent / Fallback is::
1602
+ Regex the output of::
1603
+
1604
+ openssl req -in {FILEPATH} -noout -text
1605
+ """
1606
+ log.info("parse_csr__key_technology >")
1607
+ if conditionals.cryptography:
1608
+ if TYPE_CHECKING:
1609
+ assert conditionals.EllipticCurvePublicKey is not None
1610
+ assert conditionals.RSAPublicKey is not None
1611
+ if not csr:
1612
+ csr = conditionals.cryptography.x509.load_pem_x509_csr(csr_pem.encode())
1613
+ assert csr is not None # nest under `if TYPE_CHECKING` not needed
1614
+ csr_pubkey = csr.public_key()
1615
+ assert isinstance(
1616
+ csr_pubkey, conditionals.EllipticCurvePublicKey
1617
+ ) or isinstance(csr_pubkey, conditionals.RSAPublicKey)
1618
+ return _cryptography__public_key_technology(csr_pubkey)
1619
+ log.debug(".parse_csr__key_technology > openssl fallback")
1620
+ # `openssl req -in MYCERT -noout -text`
1621
+ if not csr_pem_filepath:
1622
+ raise FallbackError_FilepathRequired("Must submit `csr_pem_filepath`.")
1623
+ if openssl_version is None:
1624
+ check_openssl_version()
1625
+
1626
+ with psutil.Popen(
1627
+ [openssl_path, "req", "-in", csr_pem_filepath, "-noout", "-text"],
1628
+ stdout=subprocess.PIPE,
1629
+ stderr=subprocess.PIPE,
1630
+ ) as proc:
1631
+ data_bytes, err = proc.communicate()
1632
+ if proc.returncode != 0:
1633
+ raise IOError("Error loading {0}: {1}".format(csr_pem_filepath, err))
1634
+ data_str = data_bytes.decode("utf8")
1635
+ return _csr_pubkey_technology__text(data_str)
1636
+
1637
+
1638
+ def parse_csr__spki_sha256(
1639
+ csr_pem: str,
1640
+ csr_pem_filepath: Optional[str] = None,
1641
+ csr: Optional["CertificateSigningRequest"] = None,
1642
+ key_technology_basic: Optional[str] = None,
1643
+ as_b64: Optional[bool] = None,
1644
+ ) -> str:
1645
+ """
1646
+ :param str csr_pem: CSR in PEM encoding
1647
+ :param str csr_pem_filepath: Optional filepath to PEM encoded CSR.
1648
+ Only used for commandline OpenSSL fallback operations.
1649
+ :param object csr: optional hint to aid in crypto commands
1650
+ :param str key_technology_basic: optional hint to aid in openssl fallback
1651
+ :param bool as_b64: encode with b64?
1652
+ :returns: spki sha256
1653
+ :rtype: str
1654
+
1655
+ The OpenSSL Equivalent / Fallback is::
1656
+
1657
+ :_see:_openssl_spki_hash_csr
1658
+ """
1659
+ log.info("parse_csr__spki_sha256 >")
1660
+ if conditionals.cryptography:
1661
+ if not csr:
1662
+ csr = conditionals.cryptography.x509.load_pem_x509_csr(csr_pem.encode())
1663
+ assert csr is not None # nest under `if TYPE_CHECKING` not needed
1664
+ publickey = csr.public_key()
1665
+ spki_sha256 = _cryptography__public_key_spki_sha256(publickey, as_b64=as_b64)
1666
+ return spki_sha256
1667
+ log.debug(".parse_csr__spki_sha256 > openssl fallback")
1668
+ if not csr_pem_filepath:
1669
+ raise FallbackError_FilepathRequired("Must submit `csr_pem_filepath`.")
1670
+ tmpfile_pem = None
1671
+ try:
1672
+ if key_technology_basic is None:
1673
+ _key_technology = parse_csr__key_technology(
1674
+ csr_pem=csr_pem, csr_pem_filepath=csr_pem_filepath
1675
+ )
1676
+ if not _key_technology:
1677
+ raise ValueError("Could not parse key_technology for backup")
1678
+ key_technology_basic = _key_technology[0]
1679
+ spki_sha256 = _openssl_spki_hash_csr(
1680
+ key_technology_basic=key_technology_basic,
1681
+ csr_pem_filepath=csr_pem_filepath,
1682
+ as_b64=as_b64,
1683
+ )
1684
+ return spki_sha256
1685
+ except Exception as exc: # noqa: F841
1686
+ raise
1687
+ finally:
1688
+ if tmpfile_pem:
1689
+ tmpfile_pem.close()
1690
+
1691
+
1692
+ def parse_csr(
1693
+ csr_pem: str,
1694
+ csr_pem_filepath: Optional[str] = None,
1695
+ ) -> CSRData:
1696
+ """
1697
+ This routine will use cryptography if available.
1698
+ If not, openssl is used via subprocesses
1699
+
1700
+ :param str csr_pem: CSR in PEM encoding
1701
+ :param str csr_pem_filepath: Optional filepath to PEM encoded CSR.
1702
+ Only used for commandline OpenSSL fallback operations.
1703
+ :returns: dict of select CSR data
1704
+ :rtype: dict
1705
+ """
1706
+ log.info("parse_csr >")
1707
+ rval: CSRData = {
1708
+ "key_technology": None,
1709
+ "spki_sha256": None,
1710
+ "SubjectAlternativeName": [],
1711
+ "subject": None,
1712
+ }
1713
+
1714
+ if conditionals.cryptography:
1715
+ csr = conditionals.cryptography.x509.load_pem_x509_csr(csr_pem.encode())
1716
+ # _subject = csr.subject.get_attributes_for_oid(cryptography.x509.oid.NameOID.COMMON_NAME)
1717
+ # _subject[0].value
1718
+ rval["subject"] = csr.subject.rfc4514_string() if csr.subject else ""
1719
+ try:
1720
+ ext = csr.extensions.get_extension_for_oid(
1721
+ conditionals.cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
1722
+ )
1723
+ if ext:
1724
+ _names = ext.value.get_values_for_type(conditionals.cryptography.x509.DNSName) # type: ignore[attr-defined]
1725
+ rval["SubjectAlternativeName"] = sorted(_names)
1726
+ except Exception as exc: # noqa: F841
1727
+ pass
1728
+ rval["key_technology"] = _cryptography__public_key_technology(csr.public_key())
1729
+ rval["spki_sha256"] = parse_csr__spki_sha256(
1730
+ csr_pem=csr_pem,
1731
+ csr_pem_filepath=csr_pem_filepath,
1732
+ csr=csr,
1733
+ as_b64=False,
1734
+ )
1735
+ return rval
1736
+
1737
+ log.debug(".parse_csr > openssl fallback")
1738
+ if openssl_version is None:
1739
+ check_openssl_version()
1740
+
1741
+ tmpfile_pem = None
1742
+ try:
1743
+ if not csr_pem_filepath:
1744
+ tmpfile_pem = new_pem_tempfile(csr_pem)
1745
+ csr_pem_filepath = tmpfile_pem.name
1746
+ _subject2 = csr_single_op__pem_filepath(csr_pem_filepath, "-subject")
1747
+ rval["subject"] = _format_openssl_components(_subject2, fieldset="subject")
1748
+ rval["key_technology"] = _key_technology = parse_csr__key_technology(
1749
+ csr_pem=csr_pem, csr_pem_filepath=csr_pem_filepath
1750
+ )
1751
+ if _key_technology:
1752
+ rval["spki_sha256"] = parse_csr__spki_sha256(
1753
+ csr_pem=csr_pem,
1754
+ csr_pem_filepath=csr_pem_filepath,
1755
+ key_technology_basic=_key_technology[0],
1756
+ as_b64=False,
1757
+ )
1758
+ with psutil.Popen(
1759
+ [openssl_path, "req", "-text", "-noout", "-in", csr_pem_filepath],
1760
+ stdout=subprocess.PIPE,
1761
+ stderr=subprocess.PIPE,
1762
+ ) as proc_text:
1763
+ data, err = proc_text.communicate()
1764
+ data = data.decode("utf8")
1765
+ found_domains = san_domains_from_text(data)
1766
+ rval["SubjectAlternativeName"] = found_domains
1767
+ return rval
1768
+ except Exception as exc: # noqa: F841
1769
+ raise
1770
+ finally:
1771
+ if tmpfile_pem:
1772
+ tmpfile_pem.close()
1773
+
1774
+
1775
+ def parse_key__spki_sha256(
1776
+ key_pem: str,
1777
+ key_pem_filepath: Optional[str] = None,
1778
+ publickey: Optional["_PublicKeySPKI"] = None,
1779
+ key_technology_basic: Optional[str] = None,
1780
+ as_b64: Optional[bool] = None,
1781
+ ) -> str:
1782
+ """
1783
+ :param str key_pem: Key in PEM form
1784
+ :param str key_pem_filepath: Optional filepath to PEM.
1785
+ Only used for commandline OpenSSL fallback operations.
1786
+ :param cryptography_publickey: optional hint to aid in crypto commands
1787
+ :type cryptography_publickey: cryptography.hazmat.backends.openssl.rsa._RSAPublicKey
1788
+ openssl_crypto.load_privatekey(...).to_cryptography_key().public_key()
1789
+ :param str key_technology_basic: optional hint to aid in openssl fallback
1790
+ :param bool as_b64: encode with b64?
1791
+ :returns: spki sha256
1792
+ :rtype: str
1793
+
1794
+ The OpenSSL Equivalent / Fallback is::
1795
+
1796
+ :_see:_openssl_spki_hash_pkey
1797
+ """
1798
+ log.info("parse_key__spki_sha256 >")
1799
+ if conditionals.cryptography:
1800
+ if TYPE_CHECKING:
1801
+ assert conditionals.crypto_serialization is not None
1802
+ if not publickey:
1803
+ privkey = conditionals.crypto_serialization.load_pem_private_key(
1804
+ key_pem.encode(), password=None
1805
+ )
1806
+ publickey = privkey.public_key() # type: ignore[union-attr]
1807
+ assert publickey is not None # nest under `if TYPE_CHECKING` not needed
1808
+ spki_sha256 = _cryptography__public_key_spki_sha256(publickey, as_b64=as_b64)
1809
+ return spki_sha256
1810
+ log.debug(".parse_key__spki_sha256 > openssl fallback")
1811
+ if not key_pem_filepath:
1812
+ raise FallbackError_FilepathRequired("Must submit `key_pem_filepath`.")
1813
+ tmpfile_pem = None
1814
+ try:
1815
+ if key_technology_basic is None:
1816
+ key_technology_basic = parse_key__technology_basic(
1817
+ key_pem=key_pem, key_pem_filepath=key_pem_filepath
1818
+ )
1819
+ spki_sha256 = _openssl_spki_hash_pkey(
1820
+ key_technology_basic=key_technology_basic,
1821
+ key_pem_filepath=key_pem_filepath,
1822
+ as_b64=as_b64,
1823
+ )
1824
+ return spki_sha256
1825
+ except Exception as exc: # noqa: F841
1826
+ raise
1827
+ finally:
1828
+ if tmpfile_pem:
1829
+ tmpfile_pem.close()
1830
+
1831
+
1832
+ def parse_key__technology_basic(
1833
+ key_pem: str,
1834
+ key_pem_filepath: Optional[str] = None,
1835
+ privatekey: Optional["_TYPES_CRYPTOGRAPHY_PRIVATEKEY"] = None,
1836
+ ) -> Literal["EC", "RSA"]:
1837
+ """
1838
+ :param str key_pem: Key in PEM form
1839
+ :param str key_pem_filepath: Optional filepath to PEM.
1840
+ Only used for commandline OpenSSL fallback operations.
1841
+ :param object privatekey: optional private key
1842
+ :returns: key technology
1843
+ :rtype: str
1844
+ """
1845
+ log.info("parse_key__technology_basic >")
1846
+ if conditionals.cryptography:
1847
+ if TYPE_CHECKING:
1848
+ assert conditionals.crypto_ec is not None
1849
+ assert conditionals.crypto_rsa is not None
1850
+ assert conditionals.crypto_serialization is not None
1851
+ if not privatekey:
1852
+ privatekey = conditionals.crypto_serialization.load_pem_private_key(
1853
+ key_pem.encode(), None
1854
+ )
1855
+ assert privatekey is not None # nest under `if TYPE_CHECKING` not needed
1856
+ if isinstance(privatekey, conditionals.crypto_rsa.RSAPrivateKey):
1857
+ # return ("RSA", (privatekey.key_size,))
1858
+ return "RSA"
1859
+ elif isinstance(privatekey, conditionals.crypto_ec.EllipticCurvePrivateKey):
1860
+ # curve_name = curve_to_nist(privatekey.curve.name)
1861
+ # return ("EC", (curve_name,))
1862
+ return "EC"
1863
+ raise OpenSslError_InvalidKey("I don't know what kind of key this is")
1864
+ log.debug(".parse_key__technology_basic > openssl fallback")
1865
+ tmpfile_pem = None
1866
+ try:
1867
+ if not key_pem_filepath:
1868
+ tmpfile_pem = new_pem_tempfile(key_pem)
1869
+ key_pem_filepath = tmpfile_pem.name
1870
+ try:
1871
+ _checked = key_single_op__pem_filepath( # noqa: F841
1872
+ "RSA", key_pem_filepath, "-check"
1873
+ )
1874
+ return "RSA"
1875
+ except OpenSslError_InvalidKey as exc1: # noqa: F841
1876
+ try:
1877
+ _checked = key_single_op__pem_filepath( # noqa: F841
1878
+ "EC", key_pem_filepath, "-check"
1879
+ )
1880
+ # TODO - parse curve
1881
+ return "EC"
1882
+ except OpenSslError_VersionTooLow as exc2: # noqa: F841
1883
+ # TODO: make this conditional
1884
+ # i doubt many people have old versions but who knows?
1885
+ raise
1886
+ except Exception as exc0: # noqa: F841
1887
+ raise
1888
+ finally:
1889
+ if tmpfile_pem:
1890
+ tmpfile_pem.close()
1891
+
1892
+
1893
+ def parse_key(
1894
+ key_pem: str,
1895
+ key_pem_filepath: Optional[str] = None,
1896
+ ) -> KeyData:
1897
+ """
1898
+ !!!: This is a debugging display function. The output is not guaranteed across installations.
1899
+
1900
+ This routine will use cryptography if available.
1901
+ If not, openssl is used via subprocesses
1902
+
1903
+ :param str key_pem: Key in PEM encoding
1904
+ :param str key_pem_filepath: Optional filepath to PEM encoded Key.
1905
+ Only used for commandline OpenSSL fallback operations.
1906
+ :returns: dict of select CSR data
1907
+ :rtype: dict
1908
+ """
1909
+ log.info("parse_key >")
1910
+ rval: KeyData = {
1911
+ "key_technology": None,
1912
+ "text": None,
1913
+ "modulus_md5": None,
1914
+ "key_technology_basic": None,
1915
+ "spki_sha256": None,
1916
+ }
1917
+ if conditionals.cryptography:
1918
+ if TYPE_CHECKING:
1919
+ assert conditionals.crypto_ec is not None
1920
+ assert conditionals.crypto_rsa is not None
1921
+ assert conditionals.crypto_serialization is not None
1922
+ try:
1923
+ # note: we don't need to provide key_pem_filepath because we already rely on openssl
1924
+ rval["key_technology"] = validate_key(key_pem=key_pem)
1925
+ except Exception as exc:
1926
+ rval["key_technology"] = str(exc)
1927
+ privkey = conditionals.crypto_serialization.load_pem_private_key(
1928
+ key_pem.encode(), password=None
1929
+ )
1930
+ publickey = privkey.public_key() # type: ignore[union-attr]
1931
+ if isinstance(privkey, conditionals.crypto_rsa.RSAPrivateKey):
1932
+ rval["key_technology_basic"] = "RSA"
1933
+ try:
1934
+ modn = publickey.public_numbers().n # type: ignore[union-attr]
1935
+ modn = "{:X}".format(modn)
1936
+ modn = modn.encode()
1937
+ rval["modulus_md5"] = hashlib.md5(modn).hexdigest()
1938
+ except Exception as exc:
1939
+ rval["XX-modulus_md5"] = str(exc)
1940
+ elif isinstance(privkey, conditionals.crypto_ec.EllipticCurvePrivateKey):
1941
+ rval["key_technology_basic"] = "EC"
1942
+ # TODO: Support EC Key Modulus Variant - https://github.com/aptise/cert_utils/issues/15
1943
+ # legacy ONLY works on RSA keys
1944
+ # might just rely on spki_sha256
1945
+ # related certbot/pyopenssl ticket:
1946
+ # see https://github.com/pyca/pyopenssl/issues/291
1947
+
1948
+ rval["spki_sha256"] = parse_key__spki_sha256(
1949
+ key_pem="",
1950
+ key_pem_filepath=None,
1951
+ publickey=publickey,
1952
+ as_b64=False,
1953
+ )
1954
+ return rval
1955
+
1956
+ log.debug(".parse_key > openssl fallback")
1957
+ tmpfile_pem = None
1958
+ try:
1959
+ if not key_pem_filepath:
1960
+ tmpfile_pem = new_pem_tempfile(key_pem)
1961
+ key_pem_filepath = tmpfile_pem.name
1962
+ try:
1963
+ rval["key_technology_basic"] = _key_technology_basic = (
1964
+ parse_key__technology_basic(
1965
+ key_pem=key_pem, key_pem_filepath=key_pem_filepath
1966
+ )
1967
+ )
1968
+ except OpenSslError_VersionTooLow as exc2: # noqa: F841
1969
+ # TODO: make this conditional
1970
+ # i doubt many people have old versions but who knows?
1971
+ raise
1972
+ try:
1973
+ # rval["check"] = key_single_op__pem_filepath(
1974
+ # _key_technology_basic, key_pem_filepath, "-check"
1975
+ # )
1976
+ rval["key_technology"] = validate_key(
1977
+ key_pem=key_pem, key_pem_filepath=key_pem_filepath
1978
+ )
1979
+ except Exception as exc1:
1980
+ rval["XX-check"] = str(exc1)
1981
+ rval["text"] = key_single_op__pem_filepath(
1982
+ _key_technology_basic, key_pem_filepath, "-text"
1983
+ )
1984
+ if _key_technology_basic in ("RSA", "EC"):
1985
+ # rval["spki_sha256"] = _openssl_spki_hash_pkey(key_technology=_key_technology, key_pem_filepath=key_pem_filepath, as_b64=False)
1986
+ rval["spki_sha256"] = parse_key__spki_sha256(
1987
+ key_pem=key_pem,
1988
+ key_pem_filepath=key_pem_filepath,
1989
+ key_technology_basic=_key_technology_basic,
1990
+ as_b64=False,
1991
+ )
1992
+
1993
+ if _key_technology_basic == "RSA":
1994
+ _modulus = key_single_op__pem_filepath(
1995
+ _key_technology_basic, key_pem_filepath, "-modulus"
1996
+ )
1997
+ _modulus = _cleanup_openssl_modulus(_modulus)
1998
+ _modulus_bytes = _modulus.encode()
1999
+ rval["modulus_md5"] = hashlib.md5(_modulus_bytes).hexdigest()
2000
+ return rval
2001
+ except Exception as exc: # noqa: F841
2002
+ raise
2003
+ finally:
2004
+ if tmpfile_pem:
2005
+ tmpfile_pem.close()
2006
+
2007
+
2008
+ def new_account_key(
2009
+ key_technology_id: Union[
2010
+ Literal[KeyTechnologyEnum.RSA, KeyTechnologyEnum.EC], Literal[1, 2]
2011
+ ] = KeyTechnologyEnum.RSA,
2012
+ rsa_bits: Optional[Literal[2048, 3072, 4096]] = 2048,
2013
+ ec_curve: Optional[Literal["P-256", "P-384"]] = "P-256",
2014
+ ) -> str:
2015
+ """
2016
+ :param int key_technology_id: Key Technology type. Default: KeyTechnology.RSA
2017
+ :param int rsa_bits: number of bits. default 2048
2018
+ :param str ec_curve: default "P-256"
2019
+ :returns: AccountKey in PEM format
2020
+ :rtype: str
2021
+ """
2022
+ if key_technology_id in (KeyTechnologyEnum.RSA, KeyTechnology.RSA):
2023
+ if rsa_bits not in ALLOWED_BITS_RSA:
2024
+ raise ValueError(
2025
+ "LetsEncrypt only supports RSA keys with bits: %s" % ALLOWED_BITS_RSA
2026
+ )
2027
+ return new_key_rsa(bits=rsa_bits)
2028
+ elif key_technology_id in (KeyTechnologyEnum.EC, KeyTechnology.EC):
2029
+ if ec_curve not in ALLOWED_CURVES_ECDSA:
2030
+ raise ValueError(
2031
+ "LetsEncrypt only supports EC with curves: %s" % ALLOWED_CURVES_ECDSA
2032
+ )
2033
+ return new_key_ec(curve=ec_curve)
2034
+ else:
2035
+ raise ValueError("invalid `key_technology_id`")
2036
+
2037
+
2038
+ def new_private_key(
2039
+ key_technology_id: Union[
2040
+ Literal[KeyTechnologyEnum.RSA, KeyTechnologyEnum.EC], Literal[1, 2]
2041
+ ],
2042
+ rsa_bits: Optional[Union[Literal[2048], Literal[3072], Literal[4096]]] = 2048,
2043
+ ec_curve: Optional[Union[Literal["P-256"], Literal["P-384"]]] = "P-384",
2044
+ ) -> str:
2045
+ """
2046
+ :param key_technology_id: Required key technology (enum or legacy integer).
2047
+ :param int rsa_bits: number of bits. default 2048
2048
+ :param str ec_curve: ec curve. default P-256
2049
+ :returns: PrivateKey in PEM format
2050
+ :rtype: str
2051
+ """
2052
+ if key_technology_id in (KeyTechnologyEnum.RSA, KeyTechnology.RSA):
2053
+ kwargs_rsa = {"bits": rsa_bits} if rsa_bits else {}
2054
+ return new_key_rsa(**kwargs_rsa)
2055
+ elif key_technology_id in (KeyTechnologyEnum.EC, KeyTechnology.EC):
2056
+ kwargs_ec = {"curve": ec_curve} if ec_curve else {}
2057
+ return new_key_ec(**kwargs_ec)
2058
+ else:
2059
+ raise ValueError("invalid `key_technology_id`")
2060
+
2061
+
2062
+ def new_key_ec(
2063
+ curve: Union[Literal["P-256"], Literal["P-384"]] = "P-256",
2064
+ ) -> str:
2065
+ """
2066
+ This routine will use cryptography if available.
2067
+ If not, openssl is used via subprocesses
2068
+
2069
+ :param str curve: Which EC curve to use
2070
+ :returns: ECDSA Key in PEM format
2071
+ :rtype: str
2072
+ """
2073
+ log.info("new_key_ec >")
2074
+ log.debug(".new_key_ec > curve = %s", curve)
2075
+ if curve not in ALLOWED_CURVES_ECDSA:
2076
+ raise ValueError(
2077
+ "LetsEncrypt only supports ECDSA keys with curves: %s; not %s"
2078
+ % (ALLOWED_CURVES_ECDSA, curve)
2079
+ )
2080
+
2081
+ if conditionals.cryptography:
2082
+ if TYPE_CHECKING:
2083
+ assert conditionals.crypto_ec is not None
2084
+ assert conditionals.crypto_serialization is not None
2085
+ # see https://github.com/pyca/pyopenssl/issues/291
2086
+ if curve == "P-256":
2087
+ key = conditionals.crypto_ec.generate_private_key(
2088
+ conditionals.crypto_ec.SECP256R1()
2089
+ )
2090
+ elif curve == "P-384":
2091
+ key = conditionals.crypto_ec.generate_private_key(
2092
+ conditionals.crypto_ec.SECP384R1()
2093
+ )
2094
+ key_pem = key.private_bytes(
2095
+ encoding=conditionals.crypto_serialization.Encoding.PEM,
2096
+ format=conditionals.crypto_serialization.PrivateFormat.TraditionalOpenSSL,
2097
+ encryption_algorithm=conditionals.crypto_serialization.NoEncryption(),
2098
+ )
2099
+ # load it: openssl_crypto.load_privatekey(openssl_crypto.FILETYPE_PEM, key_pem)
2100
+ key_pem_str = key_pem.decode("utf8")
2101
+ key_pem_str = cleanup_pem_text(key_pem_str)
2102
+ return key_pem_str
2103
+
2104
+ log.debug(".new_key_ec > openssl fallback")
2105
+ # openssl ecparam -list_curves
2106
+ _openssl_curve: str
2107
+ if curve == "P-256":
2108
+ _openssl_curve = "secp256r1"
2109
+ elif curve == "P-384":
2110
+ _openssl_curve = "secp384r1"
2111
+ else:
2112
+ raise ValueError("invalid curve")
2113
+ # openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
2114
+ # -noout will suppress printing the EC Param (see https://security.stackexchange.com/questions/29778/why-does-openssl-writes-ec-parameters-when-generating-private-key)
2115
+ if openssl_version is None:
2116
+ check_openssl_version()
2117
+
2118
+ with psutil.Popen(
2119
+ [openssl_path, "ecparam", "-name", _openssl_curve, "-genkey", "-noout"],
2120
+ stdout=subprocess.PIPE,
2121
+ stderr=subprocess.PIPE,
2122
+ ) as proc:
2123
+ data_bytes, err = proc.communicate()
2124
+ if not data_bytes:
2125
+ raise OpenSslError_InvalidKey(err)
2126
+ key_pem_str = data_bytes.decode("utf8")
2127
+ key_pem_str = cleanup_pem_text(key_pem_str)
2128
+ try:
2129
+ # we need a tmpfile to validate it
2130
+ tmpfile_pem = new_pem_tempfile(key_pem_str)
2131
+ # this will raise an error on fails
2132
+ key_technology = validate_key( # noqa: F841
2133
+ key_pem=key_pem_str, key_pem_filepath=tmpfile_pem.name
2134
+ )
2135
+ finally:
2136
+ tmpfile_pem.close()
2137
+ return key_pem_str
2138
+
2139
+
2140
+ def new_key_rsa(
2141
+ bits: Union[Literal[2048], Literal[3072], Literal[4096]] = 4096,
2142
+ ) -> str:
2143
+ """
2144
+ This routine will use cryptography if available.
2145
+ If not, openssl is used via subprocesses
2146
+
2147
+ :param int bits: number of bits. default 4096
2148
+ :returns: RSA Key in PEM format
2149
+ :rtype: str
2150
+ """
2151
+ log.info("new_key_rsa >")
2152
+ log.debug(".new_key_rsa > bits = %s", bits)
2153
+ if bits not in ALLOWED_BITS_RSA:
2154
+ raise ValueError(
2155
+ "LetsEncrypt only supports RSA keys with bits: %s; not %s"
2156
+ % (str(ALLOWED_BITS_RSA), bits)
2157
+ )
2158
+ if conditionals.cryptography:
2159
+ if TYPE_CHECKING:
2160
+ assert conditionals.crypto_rsa is not None
2161
+ assert conditionals.crypto_serialization is not None
2162
+ key_rsa = conditionals.crypto_rsa.generate_private_key(
2163
+ public_exponent=65537,
2164
+ key_size=bits,
2165
+ )
2166
+ key_pem = key_rsa.private_bytes(
2167
+ encoding=conditionals.crypto_serialization.Encoding.PEM,
2168
+ format=conditionals.crypto_serialization.PrivateFormat.TraditionalOpenSSL,
2169
+ encryption_algorithm=conditionals.crypto_serialization.NoEncryption(),
2170
+ )
2171
+ key_pem_str = key_pem.decode("utf8")
2172
+ key_pem_str = cleanup_pem_text(key_pem_str)
2173
+ return key_pem_str
2174
+ log.debug(".new_key_rsa > openssl fallback")
2175
+ # openssl genrsa 4096 > domain.key
2176
+ if openssl_version is None:
2177
+ check_openssl_version()
2178
+
2179
+ with psutil.Popen(
2180
+ [openssl_path, "genrsa", str(bits)],
2181
+ stdout=subprocess.PIPE,
2182
+ stderr=subprocess.PIPE,
2183
+ ) as proc:
2184
+ data_bytes, err = proc.communicate()
2185
+ if not data_bytes:
2186
+ raise OpenSslError_InvalidKey(err)
2187
+ key_pem_str = data_bytes.decode("utf8")
2188
+ key_pem_str = cleanup_pem_text(key_pem_str)
2189
+ try:
2190
+ # we need a tmpfile to validate it
2191
+ tmpfile_pem = new_pem_tempfile(key_pem_str)
2192
+ # this will raise an error on fails
2193
+ key_technology = validate_key( # noqa: F841
2194
+ key_pem=key_pem_str, key_pem_filepath=tmpfile_pem.name
2195
+ )
2196
+ finally:
2197
+ tmpfile_pem.close()
2198
+ return key_pem_str
2199
+
2200
+
2201
+ def cert_and_chain_from_fullchain(
2202
+ fullchain_pem: str,
2203
+ ) -> Tuple[str, str]:
2204
+ """
2205
+ Split `fullchain_pem` into `cert_pem` and `chain_pem`
2206
+
2207
+ :param str fullchain_pem: concatenated Certificate + Chain
2208
+ :returns: tuple of two PEM encoded Certificates in the format of
2209
+ (LeafCertificate, ChainedIntermediates)
2210
+ :rtype: tuple
2211
+
2212
+ This routine will use cryptography if available.
2213
+ If not, openssl is used via subprocesses
2214
+
2215
+ Portions of this are a reimplentation of certbot's code
2216
+ Certbot's code is Apache2 licensed
2217
+ https://raw.githubusercontent.com/certbot/certbot/master/LICENSE.txt
2218
+ """
2219
+ log.info("cert_and_chain_from_fullchain >")
2220
+ if conditionals.cryptography:
2221
+ try:
2222
+ return cryptography__cert_and_chain_from_fullchain(fullchain_pem)
2223
+ except Exception as exc:
2224
+ raise
2225
+ raise OpenSslError(exc)
2226
+
2227
+ log.debug(".cert_and_chain_from_fullchain > openssl fallback")
2228
+ # First pass: find the boundary of each certificate in the chain.
2229
+ # TODO: This will silently skip over any "explanatory text" in between boundaries,
2230
+ # which is prohibited by RFC8555.
2231
+ certs = split_pem_chain(fullchain_pem)
2232
+ if len(certs) < 2:
2233
+ raise OpenSslError(
2234
+ "failed to parse fullchain into cert and chain: "
2235
+ + "less than 2 certificates in chain"
2236
+ )
2237
+ # Second pass: for each certificate found, parse it using OpenSSL and re-encode it,
2238
+ # with the effect of normalizing any encoding variations (e.g. CRLF, whitespace).
2239
+ certs_normalized = []
2240
+ for _cert_pem in certs:
2241
+ _cert_pem = _openssl_cert__normalize_pem(_cert_pem)
2242
+ _cert_pem = cleanup_pem_text(_cert_pem)
2243
+ certs_normalized.append(_cert_pem)
2244
+
2245
+ # Since each normalized cert has a newline suffix, no extra newlines are required.
2246
+ return (certs_normalized[0], "".join(certs_normalized[1:]))
2247
+
2248
+
2249
+ def decompose_chain(fullchain_pem: str) -> List[str]:
2250
+ """
2251
+ Split `fullchain_pem` into multiple PEM encoded certs
2252
+
2253
+ :param str fullchain_pem: concatenated Certificate + Chain
2254
+ :returns: list of all PEM Encoded Certificates discovered in the fullchain
2255
+ :rtype: list
2256
+ """
2257
+ log.info("decompose_chain >")
2258
+ # First pass: find the boundary of each certificate in the chain.
2259
+ # TODO: This will silently skip over any "explanatory text" in between boundaries,
2260
+ # which is prohibited by RFC8555.
2261
+ certs = split_pem_chain(fullchain_pem)
2262
+ if len(certs) < 2:
2263
+ raise OpenSslError(
2264
+ "failed to parse fullchain into cert and chain: "
2265
+ + "less than 2 certificates in chain"
2266
+ )
2267
+ # Second pass: for each certificate found, parse it using OpenSSL and re-encode it,
2268
+ # with the effect of normalizing any encoding variations (e.g. CRLF, whitespace).
2269
+ if conditionals.cryptography:
2270
+ if TYPE_CHECKING:
2271
+ assert conditionals.crypto_serialization is not None
2272
+ certs_normalized = []
2273
+ for cert_pem in certs:
2274
+ cert = conditionals.cryptography.x509.load_pem_x509_certificate(
2275
+ cert_pem.encode()
2276
+ )
2277
+ cert_pem = cert.public_bytes(
2278
+ conditionals.crypto_serialization.Encoding.PEM
2279
+ ).decode()
2280
+ certs_normalized.append(cert_pem)
2281
+ return certs_normalized
2282
+ log.debug(".decompose_chain > openssl fallback")
2283
+ certs_normalized = []
2284
+ for _cert_pem in certs:
2285
+ _cert_pem = _openssl_cert__normalize_pem(_cert_pem)
2286
+ _cert_pem = cleanup_pem_text(_cert_pem)
2287
+ certs_normalized.append(_cert_pem)
2288
+ return certs_normalized
2289
+
2290
+
2291
+ def ensure_chain(
2292
+ root_pem: str,
2293
+ fullchain_pem: Optional[str] = None,
2294
+ cert_pem: Optional[str] = None,
2295
+ chain_pem: Optional[str] = None,
2296
+ root_pems_other: Optional[List[str]] = None,
2297
+ *,
2298
+ validation_time: Optional[datetime.datetime] = None,
2299
+ purpose: Literal["sslserver", "sslclient"] = "sslserver",
2300
+ server_hostname: Optional[str] = None,
2301
+ ) -> bool:
2302
+ """
2303
+ validates from a root down to a chain
2304
+ if chain is a fullchain (with endentity), cert_pem can be None
2305
+
2306
+ THIS WILL RAISE ERRORS, NOT RETURN VALUES
2307
+
2308
+ submit EITHER fullchain_pem or chain_pem+cert_pem
2309
+
2310
+ :param root_pem: The PEM Encoded Root Certificate. Required.
2311
+ :type root_pem: str
2312
+ :param fullchain_pem: A full Certificate chain in PEM encoding, which
2313
+ consists of a Leaf Certificate, and optionally multiple upstream certs in
2314
+ a single string.
2315
+ If provided:
2316
+ * `:param:cert_pem` MUST NOT be provided
2317
+ * `:param:chain_pem` MUST NOT be provided.
2318
+ :type fullchain_pem: str
2319
+ :param cert_pem: the EndEntity or Leaf Certificate.
2320
+ If provided:
2321
+ * `:param:chain_pem` MUST be provided
2322
+ * `:param:fullchain_pem` MUST NOT be provided.
2323
+ :type cert_pem: str
2324
+ :param chain_pem: A Certificate chain in PEM format, which is multiple
2325
+ upstream certs in a single string.
2326
+ If provided:
2327
+ * `:param:cert_pem` MUST be provided
2328
+ * `:param:fullchain_pem` MUST NOT be provided.
2329
+ :param root_pems_other: an iterable list of trusted roots certificates, in
2330
+ PEM encoding; currently unused.
2331
+ Both backends validate server certificate purpose by default. Pass
2332
+ purpose="sslclient" for client certificates. server_hostname optionally
2333
+ verifies a particular DNS identity; otherwise the certificate identity is used. For historical validation,
2334
+ pass an explicit timezone-aware validation_time; defaults use the current time.
2335
+
2336
+ :returns: True
2337
+ :rtype: bool
2338
+
2339
+
2340
+ The OpenSSL Equivalent / Fallback is::
2341
+
2342
+ Modern versions of openssl accept multiple `-untrusted` arguments::
2343
+
2344
+ openssl verify -purpose sslserver -CAfile root.pem [[-untrusted intermediate.pem],[-untrusted intermediate.pem],] cert.pem
2345
+
2346
+ However older ones only want to see a single `-untrusted`::
2347
+
2348
+ openssl verify -purpose sslserver -CAfile root.pem -untrusted intermediate.pem cert.pem
2349
+
2350
+ To get around this, put all the intermediates into a single file.
2351
+
2352
+ This is a stopgap solution and needs to be refactored.
2353
+
2354
+ NOTE:
2355
+ openssl does not care about the order of intermediates, so this should
2356
+ be iteratively built up like the pure-python example
2357
+ """
2358
+ log.info("ensure_chain >")
2359
+ if purpose not in ("sslserver", "sslclient"):
2360
+ raise ValueError("invalid certificate purpose")
2361
+ if validation_time is not None and validation_time.utcoffset() is None:
2362
+ raise ValueError("validation_time must be timezone-aware")
2363
+ if fullchain_pem:
2364
+ if chain_pem or cert_pem:
2365
+ raise ValueError(
2366
+ "If `ensure_chain` is invoked with `fullchain_pem`, do not pass in `chain_pem` or `cert_pem`."
2367
+ )
2368
+ else:
2369
+ if not chain_pem or not cert_pem:
2370
+ raise ValueError(
2371
+ "If `ensure_chain` is not invoked with `fullchain_pem`, you must pass in `chain_pem` and `cert_pem`."
2372
+ )
2373
+
2374
+ if fullchain_pem:
2375
+ intermediates = split_pem_chain(fullchain_pem)
2376
+ cert_pem = intermediates.pop(0)
2377
+ else:
2378
+ assert cert_pem
2379
+ assert chain_pem
2380
+ intermediates = split_pem_chain(chain_pem)
2381
+ cert_pem = cert_pem.strip() # needed to match regex results in above situation
2382
+
2383
+ # sometimes people submit things they should not
2384
+ if intermediates and intermediates[-1] == cert_pem:
2385
+ intermediates = intermediates[:-1]
2386
+
2387
+ if conditionals.cryptography:
2388
+ # build a root storage
2389
+ root_cert = conditionals.cryptography.x509.load_pem_x509_certificate(
2390
+ root_pem.encode()
2391
+ )
2392
+ store = conditionals.cryptography.x509.verification.Store(
2393
+ certs=[
2394
+ root_cert,
2395
+ ]
2396
+ )
2397
+
2398
+ intermediate_certs = []
2399
+ for _intermediate_pem in reversed(intermediates):
2400
+ _intermediate_cert = (
2401
+ conditionals.cryptography.x509.load_pem_x509_certificate(
2402
+ _intermediate_pem.encode()
2403
+ )
2404
+ )
2405
+ intermediate_certs.append(_intermediate_cert)
2406
+
2407
+ cert_cert = conditionals.cryptography.x509.load_pem_x509_certificate(
2408
+ cert_pem.encode()
2409
+ )
2410
+ builder = conditionals.cryptography.x509.verification.PolicyBuilder().store(
2411
+ store
2412
+ )
2413
+
2414
+ if validation_time is not None:
2415
+ builder = builder.time(validation_time)
2416
+ try:
2417
+ if purpose == "sslclient":
2418
+ builder.build_client_verifier().verify(cert_cert, intermediate_certs)
2419
+ else:
2420
+ names = _cryptography__cert_or_req__all_names(cert_cert)
2421
+ hostname = server_hostname or (names[0] if names else None)
2422
+ if hostname is None:
2423
+ raise ValueError("server certificate requires a DNS identity")
2424
+ verifier = builder.build_server_verifier(
2425
+ conditionals.cryptography.x509.DNSName(hostname)
2426
+ )
2427
+ verifier.verify(cert_cert, intermediate_certs)
2428
+ except Exception as exc: # noqa: F841
2429
+ raise
2430
+ return True
2431
+
2432
+ log.debug(".ensure_chain > openssl fallback")
2433
+ if openssl_version is None:
2434
+ check_openssl_version()
2435
+
2436
+ _tempfiles = []
2437
+ try:
2438
+ _tmpfile_root = new_pem_tempfile(root_pem)
2439
+ _tempfiles.append(_tmpfile_root)
2440
+
2441
+ intermediates_unified = "\n".join(intermediates)
2442
+ _tempfile_intermediate = new_pem_tempfile(intermediates_unified)
2443
+ _tempfiles.append(_tempfile_intermediate)
2444
+
2445
+ _tmpfile_cert = new_pem_tempfile(cert_pem)
2446
+ _tempfiles.append(_tmpfile_cert)
2447
+
2448
+ expected_success = "%s: OK\n" % _tmpfile_cert.name
2449
+ with psutil.Popen(
2450
+ [
2451
+ openssl_path,
2452
+ "verify",
2453
+ "-purpose",
2454
+ purpose,
2455
+ "-CAfile",
2456
+ _tmpfile_root.name,
2457
+ "-untrusted",
2458
+ _tempfile_intermediate.name,
2459
+ ]
2460
+ + (
2461
+ ["-attime", str(int(validation_time.timestamp()))]
2462
+ if validation_time is not None
2463
+ else []
2464
+ )
2465
+ + (
2466
+ ["-verify_hostname", server_hostname]
2467
+ if server_hostname is not None and purpose == "sslserver"
2468
+ else []
2469
+ )
2470
+ + [_tmpfile_cert.name],
2471
+ stdout=subprocess.PIPE,
2472
+ stderr=subprocess.PIPE,
2473
+ ) as proc:
2474
+ data, err = proc.communicate()
2475
+ if err:
2476
+ raise OpenSslError(err.decode("utf8", errors="replace"))
2477
+ data = data.decode("utf8")
2478
+ if data != expected_success:
2479
+ raise OpenSslError("could not verify: 2")
2480
+ return True
2481
+ finally:
2482
+ for _tmp in _tempfiles:
2483
+ _tmp.close()
2484
+
2485
+
2486
+ def ensure_chain_order(
2487
+ chain_certs: List[str],
2488
+ cert_pem: Optional[str] = None,
2489
+ ) -> bool:
2490
+ """
2491
+ :param chain_certs: A list of PEM encoded Certificates. Required.
2492
+ :type chain_certs: list
2493
+ :param cert_pem: A PEM Encoded Certificate to test against the `chain_certs`.
2494
+ Optional
2495
+ :type cert_pem: str
2496
+ :returns: bool
2497
+ :rtype: bool
2498
+
2499
+ Compares issuer/subject adjacency only on both backends.
2500
+ Use ensure_chain for trust, signature, purpose and expiry validation.
2501
+ """
2502
+ log.debug("ensure_chain_order >")
2503
+ if cert_pem:
2504
+ chain_certs = chain_certs + [cert_pem]
2505
+ if len(chain_certs) < 2:
2506
+ raise ValueError("must submit 2 or more chain certificates")
2507
+ # reverse the cert list
2508
+ # we're going to pretend the last item is a root
2509
+ r_chain_certs = chain_certs[::-1]
2510
+ if conditionals.cryptography:
2511
+ # TODO: openssl crypto does not seem to support partial chains yet
2512
+ # as a stopgap, just look to ensure the issuer/subject match
2513
+ """
2514
+ # build a root storage
2515
+ # pretend the first item is a root
2516
+ store = openssl_crypto.X509Store()
2517
+ root_pem = r_chain_certs.pop(0)
2518
+ root_parsed = openssl_crypto.load_certificate(openssl_crypto.FILETYPE_PEM, root_pem)
2519
+ store.add_cert(root_parsed)
2520
+
2521
+ for (idx, cert_pem) in enumerate(r_chain_certs):
2522
+ # Check the chain certificate before adding it to the store.
2523
+ try:
2524
+ cert_parsed = openssl_crypto.load_certificate(openssl_crypto.FILETYPE_PEM, cert_pem)
2525
+ _store_ctx = openssl_crypto.X509StoreContext(store, cert_parsed)
2526
+ _store_ctx.verify_certificate()
2527
+ store.add_cert(cert_parsed)
2528
+ except openssl_crypto.X509StoreContextError as exc:
2529
+ raise OpenSslError("could not verify: crypto")
2530
+ """
2531
+ # stash our data in here
2532
+ parsed_certs = {}
2533
+
2534
+ # loop the certs
2535
+ for idx, cert_pem in enumerate(r_chain_certs):
2536
+ # everyone generates data
2537
+ cert = conditionals.cryptography.x509.load_pem_x509_certificate(
2538
+ cert_pem.encode()
2539
+ )
2540
+ parsed_certs[idx] = cert
2541
+ if idx == 0:
2542
+ continue
2543
+ # only after the first cert do we need to check the last cert
2544
+ upchain = parsed_certs[idx - 1]
2545
+ if upchain.subject != cert.issuer:
2546
+ raise OpenSslError("could not verify: upchain does not match issuer")
2547
+ return True
2548
+ log.debug(".ensure_chain_order > openssl fallback")
2549
+ if openssl_version is None:
2550
+ check_openssl_version()
2551
+
2552
+ previous_subject = None
2553
+ for pem in r_chain_certs:
2554
+ tmp = new_pem_tempfile(pem)
2555
+ try:
2556
+ with psutil.Popen(
2557
+ [
2558
+ openssl_path,
2559
+ "x509",
2560
+ "-in",
2561
+ tmp.name,
2562
+ "-noout",
2563
+ "-subject",
2564
+ "-issuer",
2565
+ ],
2566
+ stdout=subprocess.PIPE,
2567
+ stderr=subprocess.PIPE,
2568
+ ) as proc:
2569
+ output, err = proc.communicate()
2570
+ if proc.returncode:
2571
+ raise OpenSslError(err.decode("utf8", errors="replace"))
2572
+ fields = dict(line.split("=", 1) for line in output.decode().splitlines())
2573
+ subject = fields["subject"].strip()
2574
+ issuer = fields["issuer"].strip()
2575
+ if previous_subject is not None and previous_subject != issuer:
2576
+ raise OpenSslError("could not verify: upchain does not match issuer")
2577
+ previous_subject = subject
2578
+ finally:
2579
+ tmp.close()
2580
+ return True
2581
+
2582
+
2583
+ def account_key__parse(
2584
+ key_pem: str,
2585
+ key_pem_filepath: Optional[str] = None,
2586
+ ) -> Tuple[Dict[str, str], str, str]:
2587
+ """
2588
+ :param key_pem: (required) the RSA Key in PEM format
2589
+ :param key_pem_filepath: Optional filepath to a PEM encoded RSA account key file.
2590
+ Only used for commandline OpenSSL fallback operations.
2591
+ :returns: jwk, thumbprint, alg
2592
+ :rtype: list
2593
+
2594
+ This routine will use cryptography if available.
2595
+ If not, openssl is used via subprocesses
2596
+
2597
+ This includes code from acme-tiny [https://github.com/diafygi/acme-tiny]
2598
+ acme-tiny is released under the MIT license and Copyright (c) 2015 Daniel Roesler
2599
+ """
2600
+ log.info("account_key__parse >")
2601
+ _key_technology_basic = parse_key__technology_basic(
2602
+ key_pem=key_pem, key_pem_filepath=key_pem_filepath
2603
+ )
2604
+ if _key_technology_basic == "RSA":
2605
+ alg = "RS256"
2606
+ elif _key_technology_basic == "EC":
2607
+ alg = "ES256"
2608
+ else:
2609
+ raise ValueError("invalid key_technology")
2610
+ if conditionals.josepy:
2611
+ if _key_technology_basic == "RSA":
2612
+ _jwk = conditionals.josepy.JWKRSA.load(key_pem.encode("utf8"))
2613
+ jwk = _jwk.public_key().fields_to_partial_json()
2614
+ jwk["kty"] = "RSA"
2615
+ elif _key_technology_basic == "EC":
2616
+ _jwk = conditionals.josepy.JWKEC.load(key_pem.encode("utf8"))
2617
+ jwk = _jwk.public_key().fields_to_partial_json()
2618
+ jwk["kty"] = "EC"
2619
+ """
2620
+ jwk will be something like:
2621
+ {'crv': 'P-256',
2622
+ 'x': '...',
2623
+ 'y': '...'}
2624
+ # is this needed?
2625
+ if jwk["crv"] == "P-256":
2626
+ alg = "ES256"
2627
+ elif jwk["crv"] == "P-384":
2628
+ alg = "ES384"
2629
+ else:
2630
+ raise ValueError("unknown curve")
2631
+ """
2632
+
2633
+ thumbprint = jose_b64(_jwk.thumbprint())
2634
+ return jwk, thumbprint, alg
2635
+ log.debug(".account_key__parse > openssl fallback")
2636
+ if openssl_version is None:
2637
+ check_openssl_version()
2638
+
2639
+ _tmpfile = None
2640
+ try:
2641
+ if _key_technology_basic == "RSA":
2642
+ if key_pem_filepath is None:
2643
+ _tmpfile = new_pem_tempfile(key_pem)
2644
+ key_pem_filepath = _tmpfile.name
2645
+ with psutil.Popen(
2646
+ [
2647
+ openssl_path,
2648
+ "rsa",
2649
+ "-in",
2650
+ key_pem_filepath,
2651
+ "-noout",
2652
+ "-text",
2653
+ ],
2654
+ stdin=subprocess.PIPE,
2655
+ stdout=subprocess.PIPE,
2656
+ stderr=subprocess.PIPE,
2657
+ ) as proc:
2658
+ data_bytes, err = proc.communicate()
2659
+ data_str = data_bytes.decode("utf8")
2660
+ assert data_str
2661
+ pub_pattern = r"modulus:[\s]+?00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)"
2662
+ _matched = re.search(pub_pattern, data_str, re.MULTILINE | re.DOTALL)
2663
+ assert _matched
2664
+ pub_hex, pub_exp = _matched.groups()
2665
+ pub_exp = "{0:x}".format(int(pub_exp))
2666
+ pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
2667
+ jwk = {
2668
+ "e": jose_b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
2669
+ "kty": "RSA",
2670
+ "n": jose_b64(
2671
+ binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))
2672
+ ),
2673
+ }
2674
+ _accountkey_json = json.dumps(jwk, sort_keys=True, separators=(",", ":"))
2675
+ elif _key_technology_basic == "EC":
2676
+ if key_pem_filepath is None:
2677
+ _tmpfile = new_pem_tempfile(key_pem)
2678
+ key_pem_filepath = _tmpfile.name
2679
+ with psutil.Popen(
2680
+ [
2681
+ openssl_path,
2682
+ "ec",
2683
+ "-in",
2684
+ key_pem_filepath,
2685
+ "-noout",
2686
+ "-text",
2687
+ ],
2688
+ stdin=subprocess.PIPE,
2689
+ stdout=subprocess.PIPE,
2690
+ stderr=subprocess.PIPE,
2691
+ ) as proc:
2692
+ data_bytes, err = proc.communicate()
2693
+ data_str = data_bytes.decode("utf8")
2694
+ assert data_str
2695
+ # keys generated by account_key__new don't have the NIST embedded?
2696
+ pub_pattern = r"pub:[\s]+?([a-f0-9\:\s]+?)\nASN1 OID:[\s]+([\w\-]+)\n(?:NIST CURVE:[\s]+([\w\-]+)\n)?"
2697
+ _matched = re.search(pub_pattern, data_str, re.MULTILINE | re.DOTALL)
2698
+ assert _matched
2699
+ pub_hex, ans1, nist = _matched.groups()
2700
+ pub_hex = "".join([i.strip() for i in pub_hex.split("\n")])
2701
+ if nist is None:
2702
+ if ans1.lower() == "secp256r1":
2703
+ nist = "P-256"
2704
+ elif ans1.lower() == "secp384r1":
2705
+ nist = "P-384"
2706
+ if nist == "P-256":
2707
+ alg = "EC256"
2708
+ elif nist == "P-384":
2709
+ alg = "EC384"
2710
+ else:
2711
+ raise ValueError("unknown curve")
2712
+ _pub_hex = pub_hex.replace(":", "")
2713
+ # this is a compressed key and we should never see it in this context
2714
+ assert len(_pub_hex) > 66
2715
+ # Uncompressed key
2716
+ x = int(_pub_hex[:64], 16)
2717
+ y = int(_pub_hex[64:], 16)
2718
+ jwk = {
2719
+ "kty": "EC",
2720
+ "crv": nist,
2721
+ "x": x,
2722
+ "y": y,
2723
+ }
2724
+ _accountkey_json = json.dumps(jwk, sort_keys=True, separators=(",", ":"))
2725
+ else:
2726
+ raise ValueError("invalid key_technology")
2727
+ thumbprint = jose_b64(hashlib.sha256(_accountkey_json.encode("utf8")).digest())
2728
+ return jwk, thumbprint, alg
2729
+ finally:
2730
+ if _tmpfile:
2731
+ _tmpfile.close()
2732
+
2733
+
2734
+ def account_key__sign(
2735
+ data: Union[str, bytes],
2736
+ key_pem: str,
2737
+ key_pem_filepath: Optional[str] = None,
2738
+ standardize_signature: Optional[bool] = True,
2739
+ ) -> bytes:
2740
+ """
2741
+ This routine will use cryptography if available.
2742
+ If not, openssl is used via subprocesses
2743
+
2744
+ :param key_pem: (required) the RSA Key in PEM format
2745
+ :param key_pem_filepath: Optional filepath to a PEM encoded RSA account key file.
2746
+ Only used for commandline OpenSSL fallback operations.
2747
+ :standardize_signature: For EC, True returns fixed-width r||s; False/None
2748
+ returns ASN.1 DER. RSA is unaffected. SHA-256 is used on both backends.
2749
+ :returns: signature
2750
+ :rtype: bytes
2751
+ """
2752
+ log.info("account_key__sign >")
2753
+ if not isinstance(data, bytes):
2754
+ data = data.encode()
2755
+ if conditionals.cryptography and conditionals.josepy:
2756
+ if TYPE_CHECKING:
2757
+ assert conditionals.crypto_ec is not None
2758
+ assert conditionals.crypto_hashes is not None
2759
+ assert conditionals.crypto_rsa is not None
2760
+ assert conditionals.crypto_serialization is not None
2761
+ assert conditionals.crypto_utils is not None
2762
+ pkey = conditionals.crypto_serialization.load_pem_private_key(
2763
+ key_pem.encode(), None
2764
+ )
2765
+ if isinstance(pkey, conditionals.crypto_ec.EllipticCurvePrivateKey):
2766
+ signature = pkey.sign( # type: ignore[union-attr, call-arg]
2767
+ data,
2768
+ conditionals.crypto_ec.ECDSA(conditionals.crypto_hashes.SHA256()),
2769
+ )
2770
+ if standardize_signature:
2771
+ # https://community.letsencrypt.org/t/debugging-pebble-ec-account-keys/231109/3
2772
+ dr, ds = conditionals.crypto_utils.decode_dss_signature(signature)
2773
+ length = conditionals.josepy.jwk.JWKEC.expected_length_for_curve(
2774
+ pkey.curve
2775
+ )
2776
+ signature = dr.to_bytes(length=length, byteorder="big") + ds.to_bytes(
2777
+ length=length, byteorder="big"
2778
+ )
2779
+ elif isinstance(pkey, conditionals.crypto_rsa.RSAPrivateKey):
2780
+ signature = pkey.sign( # type: ignore[union-attr, call-arg]
2781
+ data,
2782
+ conditionals.cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15(), # type: ignore[arg-type]
2783
+ conditionals.crypto_hashes.SHA256(),
2784
+ )
2785
+ else:
2786
+ raise ValueError("unsupported private key type")
2787
+ return signature
2788
+ log.debug(".account_key__sign > openssl fallback")
2789
+ if openssl_version is None:
2790
+ check_openssl_version()
2791
+
2792
+ _tmpfile = None
2793
+ try:
2794
+ if key_pem_filepath is None:
2795
+ _tmpfile = new_pem_tempfile(key_pem)
2796
+ key_pem_filepath = _tmpfile.name
2797
+ width = _openssl_account_key_width(openssl_path, key_pem_filepath)
2798
+ with psutil.Popen(
2799
+ [openssl_path, "dgst", "-sha256", "-sign", key_pem_filepath],
2800
+ stdin=subprocess.PIPE,
2801
+ stdout=subprocess.PIPE,
2802
+ stderr=subprocess.PIPE,
2803
+ ) as proc:
2804
+ signature, err = proc.communicate(data)
2805
+ if proc.returncode != 0:
2806
+ raise IOError("account_key__sign\n{0}".format(err))
2807
+ if width is not None and standardize_signature:
2808
+ signature = _ecdsa_der_to_raw(signature, width)
2809
+ return signature
2810
+ finally:
2811
+ if _tmpfile:
2812
+ _tmpfile.close()
2813
+
2814
+
2815
+ def account_key__verify(
2816
+ signature: Union[str, bytes],
2817
+ data: Union[str, bytes],
2818
+ key_pem: str,
2819
+ key_pem_filepath: Optional[str] = None,
2820
+ standardize_signature: Optional[bool] = True,
2821
+ ) -> None:
2822
+ """
2823
+ This routine will use cryptography if available.
2824
+ If not, openssl is used via subprocesses
2825
+
2826
+ :param key_pem: (required) the RSA Key in PEM format
2827
+ :param key_pem_filepath: Optional filepath to a PEM encoded RSA account key file.
2828
+ Only used for commandline OpenSSL fallback operations.
2829
+ :param standardize_signature: For EC, True expects fixed-width r||s;
2830
+ False/None expects ASN.1 DER. RSA is unaffected. No format guessing.
2831
+ :returns: None on success; invalid signatures raise.
2832
+ Invalid OpenSSL verifications raise OpenSslError_InvalidSignature;
2833
+ the Python backend retains Cryptography InvalidSignature.
2834
+ :rtype: None
2835
+ """
2836
+ log.info("account_key__verify >")
2837
+ if not isinstance(signature, bytes):
2838
+ signature = signature.encode()
2839
+ if not isinstance(data, bytes):
2840
+ data = data.encode()
2841
+ if conditionals.cryptography and conditionals.josepy:
2842
+ if TYPE_CHECKING:
2843
+ assert conditionals.crypto_ec is not None
2844
+ assert conditionals.crypto_hashes is not None
2845
+ assert conditionals.crypto_rsa is not None
2846
+ assert conditionals.crypto_serialization is not None
2847
+ assert conditionals.crypto_utils is not None
2848
+ pkey = conditionals.crypto_serialization.load_pem_private_key(
2849
+ key_pem.encode(), None
2850
+ )
2851
+ if isinstance(pkey, conditionals.crypto_ec.EllipticCurvePrivateKey):
2852
+ if standardize_signature:
2853
+ # https://community.letsencrypt.org/t/debugging-pebble-ec-account-keys/231109/3
2854
+ # https://github.com/certbot/josepy/blob/2731969a8460a3ae0dcbcdc65be772385eb4a89e/src/josepy/jwa.py#L168
2855
+ rlen = conditionals.josepy.jwk.JWKEC.expected_length_for_curve(
2856
+ pkey.curve
2857
+ )
2858
+ if len(signature) != 2 * rlen:
2859
+ # Format error - rfc7518 - 3.4 | MUST NOT be shortened to omit any leading zero octets
2860
+ raise ValueError("invalid signature")
2861
+ asn1sig = conditionals.crypto_utils.encode_dss_signature(
2862
+ int.from_bytes(signature[0:rlen], byteorder="big"),
2863
+ int.from_bytes(signature[rlen:], byteorder="big"),
2864
+ )
2865
+ signature = asn1sig
2866
+ pkey.public_key().verify(
2867
+ signature,
2868
+ data,
2869
+ conditionals.crypto_ec.ECDSA(conditionals.crypto_hashes.SHA256()),
2870
+ )
2871
+ elif isinstance(pkey, conditionals.crypto_rsa.RSAPrivateKey):
2872
+ pkey.public_key().verify(
2873
+ signature,
2874
+ data,
2875
+ conditionals.cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15(), # type: ignore[arg-type]
2876
+ conditionals.crypto_hashes.SHA256(),
2877
+ )
2878
+ else:
2879
+ raise ValueError("unsupported private key type")
2880
+ return None
2881
+ log.debug(".account_key__verify > openssl fallback")
2882
+ if openssl_version is None:
2883
+ check_openssl_version()
2884
+ with tempfile.TemporaryDirectory(prefix="cert-utils-verify-") as folder:
2885
+ if key_pem_filepath is None:
2886
+ key_pem_filepath = os.path.join(folder, "key.pem")
2887
+ descriptor = os.open(
2888
+ key_pem_filepath, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600
2889
+ )
2890
+ with os.fdopen(descriptor, "wb") as key_file:
2891
+ key_file.write(key_pem.encode())
2892
+ width = _openssl_account_key_width(openssl_path, key_pem_filepath)
2893
+ if width is not None and standardize_signature:
2894
+ signature = _ecdsa_raw_to_der(signature, width)
2895
+ signature_path = os.path.join(folder, "signature.bin")
2896
+ with open(signature_path, "wb") as signature_file:
2897
+ signature_file.write(signature)
2898
+ with psutil.Popen(
2899
+ [
2900
+ openssl_path,
2901
+ "dgst",
2902
+ "-sha256",
2903
+ "-prverify",
2904
+ key_pem_filepath,
2905
+ "-signature",
2906
+ signature_path,
2907
+ ],
2908
+ stdin=subprocess.PIPE,
2909
+ stdout=subprocess.PIPE,
2910
+ stderr=subprocess.PIPE,
2911
+ ) as proc:
2912
+ output, error = proc.communicate(data)
2913
+ if proc.returncode:
2914
+ raise OpenSslError_InvalidSignature(
2915
+ (error or output).decode("utf8", errors="replace")
2916
+ )
2917
+ return None
2918
+
2919
+
2920
+ def ari__encode_serial_no(serial_no: int) -> str:
2921
+ # we need one more byte when aligend due to sign padding
2922
+ _serial_url = serial_no.to_bytes((serial_no.bit_length() + 8) // 8, "big")
2923
+ serial_url = base64.urlsafe_b64encode(_serial_url).decode("ascii").replace("=", "")
2924
+ return serial_url
2925
+
2926
+
2927
+ def ari_construct_identifier(
2928
+ cert_pem: str,
2929
+ cert_pem_filepath: Optional[str] = None,
2930
+ ) -> str:
2931
+ """
2932
+ construct an ARI key identifier
2933
+
2934
+ This is quite a PAIN.
2935
+
2936
+ All the relevant info is in the Certificate itself, but requires extended
2937
+ parsing as Python libraries overparse or underparse the relevant data
2938
+ structures.
2939
+
2940
+ In a first ARI client draft to Certbot, a LetsEncrypt engineer constructs
2941
+ an OSCP request to make this data more acessible:
2942
+ https://github.com/certbot/certbot/pull/9102/files
2943
+
2944
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2945
+ from cryptography.hazmat.primitives import hashes
2946
+ from cryptography.x509 import ocsp
2947
+ import josepy as jose
2948
+
2949
+ def get_renewal_info(self, cert: jose.ComparableX509, issuer: jose.ComparableX509) -> messages.RenewalInfoResource:
2950
+ '''Fetch ACME Renewal Information for certificate.
2951
+ :param .ComparableX509 cert: The cert whose renewal info should be fetched.
2952
+ :param .ComparableX509 issuer: The intermediate which issued the above cert,
2953
+ which will be used to uniquely identify the cert in the ARI request.
2954
+ '''
2955
+ # Rather than compute the serial, issuer key hash, and issuer name hash
2956
+ # ourselves, we instead build an OCSP Request and extract those fields.
2957
+ builder = ocsp.OCSPRequestBuilder()
2958
+ builder = builder.add_certificate(cert, issuer, hashes.SHA1())
2959
+ ocspRequest = builder.build()
2960
+
2961
+ # Construct the ARI path from the OCSP CertID sequence.
2962
+ key_hash = ocspRequest.issuer_key_hash.hex()
2963
+ name_hash = ocspRequest.issuer_name_hash.hex()
2964
+ serial = hex(ocspRequest.serial_number)[2:]
2965
+ path = f"{key_hash}/{name_hash}/{serial}"
2966
+
2967
+ return self.net.get(self.directory['renewalInfo'].rstrip('/') + '/' + path)
2968
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2969
+
2970
+ I want to avoid the OCSP generation in this routine, because that requires
2971
+ having the Intermediate - and that is really out-of-scope for the purposes
2972
+ of this function.
2973
+
2974
+ I came up with the same approach as @orangepizza in this Certbot PR:
2975
+ https://github.com/certbot/certbot/pull/9945
2976
+
2977
+ Originally I had parsed the data out using `asn1`, but I didn't want to have
2978
+ that dependency, so I implemented @orangepizza's idea of discarding the first
2979
+ 4 bytes, as they are guaranteed to be the tag.
2980
+
2981
+ LetsEncrypt engineer @aarongable doesn't think that is safe enough, and
2982
+ believes the data should be fully parsed.
2983
+
2984
+ As a temporary compromise until I weighed options better, I implemented a
2985
+ PREFERNCE to utilize asn1 decoding if the package is installed, with a
2986
+ FALLBACK to just discarding the first 4 bits if it is not available.
2987
+
2988
+ After implementing that, I realized the underlying issue was using the
2989
+ openssl certificate object - which is quite kludgy. I migrated the function
2990
+ to use the cryptography package's Certificate object, which offers a much
2991
+ cleaner and more reliable way to extract this data.
2992
+ """
2993
+ log.info("ari_construct_identifier >")
2994
+
2995
+ if conditionals.cryptography:
2996
+ try:
2997
+ cert = conditionals.cryptography.x509.load_pem_x509_certificate(
2998
+ cert_pem.encode()
2999
+ )
3000
+ except Exception as exc:
3001
+ raise CryptographyError(exc)
3002
+
3003
+ akid = None
3004
+ try:
3005
+ ext = cert.extensions.get_extension_for_oid(
3006
+ conditionals.cryptography.x509.oid.ExtensionOID.AUTHORITY_KEY_IDENTIFIER
3007
+ )
3008
+ akid = ext.value.key_identifier
3009
+ except Exception as exc:
3010
+ log.debug("Exception", exc)
3011
+ if not akid:
3012
+ raise ValueError("akid: not found")
3013
+
3014
+ akid_url = base64.urlsafe_b64encode(akid).decode("ascii").replace("=", "")
3015
+
3016
+ serial_no = cert.serial_number
3017
+ if not isinstance(serial_no, int):
3018
+ raise ValueError("serial: expected integer")
3019
+ serial_url = ari__encode_serial_no(serial_no)
3020
+
3021
+ return f"{akid_url}.{serial_url}"
3022
+
3023
+ log.debug(".ari_construct_identifier > openssl fallback")
3024
+
3025
+ # generate `cert_pem_filepath` if needed.
3026
+ _tmpfile_cert = None
3027
+ if not cert_pem_filepath:
3028
+ _tmpfile_cert = new_pem_tempfile(cert_pem)
3029
+ cert_pem_filepath = _tmpfile_cert.name
3030
+ try:
3031
+ # openssl x509 -in {CERTIFICATE} -inform pem -noout -text
3032
+ with psutil.Popen(
3033
+ [
3034
+ openssl_path,
3035
+ "x509",
3036
+ "-in",
3037
+ cert_pem_filepath,
3038
+ "-inform",
3039
+ "PEM",
3040
+ "-noout",
3041
+ "-text",
3042
+ ],
3043
+ stdout=subprocess.PIPE,
3044
+ stderr=subprocess.PIPE,
3045
+ ) as proc:
3046
+ data_bytes, err = proc.communicate()
3047
+ if not data_bytes:
3048
+ raise OpenSslError_InvalidCertificate(err)
3049
+ # this may be True or bytes, depending on the version
3050
+ # in any event, being here means we passed
3051
+
3052
+ data_str = data_bytes.decode()
3053
+ akid = authority_key_identifier_from_text(data_str)
3054
+ if not akid:
3055
+ raise ValueError("akid: not found")
3056
+ serial_no = serial_from_text(data_str)
3057
+ if not serial_no:
3058
+ raise ValueError("serial: not found")
3059
+
3060
+ akid_url = (
3061
+ base64.urlsafe_b64encode(bytes.fromhex(akid))
3062
+ .decode("ascii")
3063
+ .replace("=", "")
3064
+ )
3065
+
3066
+ serial_url = ari__encode_serial_no(serial_no)
3067
+
3068
+ return f"{akid_url}.{serial_url}"
3069
+
3070
+ finally:
3071
+ if _tmpfile_cert:
3072
+ _tmpfile_cert.close()
3073
+
3074
+
3075
+ # ------------------------------------------------------------------------------