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.
- cert_utils/__init__.py +8 -0
- cert_utils/compat/__init__.py +0 -0
- cert_utils/compat/cryptography.py +172 -0
- cert_utils/compat/openssl.py +860 -0
- cert_utils/compat/pyopenssl.py +207 -0
- cert_utils/compat/signatures.py +114 -0
- cert_utils/conditionals.py +121 -0
- cert_utils/convert.py +307 -0
- cert_utils/core.py +3075 -0
- cert_utils/errors.py +54 -0
- cert_utils/letsencrypt-certs/deprecated/e5-cross.pem +26 -0
- cert_utils/letsencrypt-certs/deprecated/e5.pem +17 -0
- cert_utils/letsencrypt-certs/deprecated/e6-cross.pem +26 -0
- cert_utils/letsencrypt-certs/deprecated/e6.pem +17 -0
- cert_utils/letsencrypt-certs/deprecated/e7-cross.pem +26 -0
- cert_utils/letsencrypt-certs/deprecated/e7.pem +17 -0
- cert_utils/letsencrypt-certs/deprecated/e8-cross.pem +26 -0
- cert_utils/letsencrypt-certs/deprecated/e8.pem +17 -0
- cert_utils/letsencrypt-certs/deprecated/e9-cross.pem +26 -0
- cert_utils/letsencrypt-certs/deprecated/e9.pem +17 -0
- cert_utils/letsencrypt-certs/deprecated/fakeleintermediatex1.pem +28 -0
- cert_utils/letsencrypt-certs/deprecated/fakelerootx1.pem +29 -0
- cert_utils/letsencrypt-certs/deprecated/isrg-root-ocsp-x1.pem +28 -0
- cert_utils/letsencrypt-certs/deprecated/isrg-root-x1-cross-signed.pem +31 -0
- cert_utils/letsencrypt-certs/deprecated/isrg-root-x2-cross-signed.pem +26 -0
- cert_utils/letsencrypt-certs/deprecated/lets-encrypt-e1.pem +17 -0
- cert_utils/letsencrypt-certs/deprecated/lets-encrypt-e2.pem +17 -0
- cert_utils/letsencrypt-certs/deprecated/lets-encrypt-r3-cross-signed.pem +26 -0
- cert_utils/letsencrypt-certs/deprecated/lets-encrypt-r3.pem +30 -0
- cert_utils/letsencrypt-certs/deprecated/lets-encrypt-r4-cross-signed.pem +26 -0
- cert_utils/letsencrypt-certs/deprecated/lets-encrypt-r4.pem +30 -0
- cert_utils/letsencrypt-certs/deprecated/lets-encrypt-x1-cross-signed.pem +27 -0
- cert_utils/letsencrypt-certs/deprecated/lets-encrypt-x2-cross-signed.pem +27 -0
- cert_utils/letsencrypt-certs/deprecated/lets-encrypt-x3-cross-signed.pem +27 -0
- cert_utils/letsencrypt-certs/deprecated/lets-encrypt-x4-cross-signed.pem +27 -0
- cert_utils/letsencrypt-certs/deprecated/letsencrypt-stg-root-x2-signed-by-x1.pem +27 -0
- cert_utils/letsencrypt-certs/deprecated/letsencryptauthorityx1.pem +32 -0
- cert_utils/letsencrypt-certs/deprecated/letsencryptauthorityx2.pem +32 -0
- cert_utils/letsencrypt-certs/deprecated/letsencryptauthorityx3.pem +32 -0
- cert_utils/letsencrypt-certs/deprecated/letsencryptauthorityx4.pem +32 -0
- cert_utils/letsencrypt-certs/deprecated/r10.pem +29 -0
- cert_utils/letsencrypt-certs/deprecated/r11.pem +29 -0
- cert_utils/letsencrypt-certs/deprecated/r12.pem +29 -0
- cert_utils/letsencrypt-certs/deprecated/r13.pem +29 -0
- cert_utils/letsencrypt-certs/deprecated/r14.pem +29 -0
- cert_utils/letsencrypt-certs/deprecated/trustid-x3-root.p7c +0 -0
- cert_utils/letsencrypt-certs/deprecated/trustid-x3-root.pem +20 -0
- cert_utils/letsencrypt-certs/int-ye1.pem +16 -0
- cert_utils/letsencrypt-certs/int-ye2.pem +16 -0
- cert_utils/letsencrypt-certs/int-ye3.pem +16 -0
- cert_utils/letsencrypt-certs/int-yr1.pem +28 -0
- cert_utils/letsencrypt-certs/int-yr2.pem +28 -0
- cert_utils/letsencrypt-certs/int-yr3.pem +28 -0
- cert_utils/letsencrypt-certs/isrg-root-x2.pem +14 -0
- cert_utils/letsencrypt-certs/isrgrootx1.pem +31 -0
- cert_utils/letsencrypt-certs/isrgrootx1.pkix +0 -0
- cert_utils/letsencrypt-certs/letsencrypt-stg-root-x1.pem +32 -0
- cert_utils/letsencrypt-certs/letsencrypt-stg-root-x2.pem +15 -0
- cert_utils/letsencrypt-certs/root-x2-by-x1.pem +26 -0
- cert_utils/letsencrypt-certs/root-ye-by-x2.pem +17 -0
- cert_utils/letsencrypt-certs/root-ye.pem +12 -0
- cert_utils/letsencrypt-certs/root-yr-by-x1.pem +34 -0
- cert_utils/letsencrypt-certs/root-yr.pem +30 -0
- cert_utils/letsencrypt_info/__init__.py +6 -0
- cert_utils/letsencrypt_info/certificates.py +1074 -0
- cert_utils/letsencrypt_info/rate_limits.py +414 -0
- cert_utils/letsencrypt_info/types.py +120 -0
- cert_utils/model.py +181 -0
- cert_utils/network.py +28 -0
- cert_utils/py.typed +0 -0
- cert_utils/scripts/__init__.py +0 -0
- cert_utils/scripts/cert_info.py +332 -0
- cert_utils/utils.py +388 -0
- cert_utils-1.2.0.dist-info/METADATA +259 -0
- cert_utils-1.2.0.dist-info/RECORD +79 -0
- cert_utils-1.2.0.dist-info/WHEEL +5 -0
- cert_utils-1.2.0.dist-info/entry_points.txt +2 -0
- cert_utils-1.2.0.dist-info/licenses/LICENSE.txt +224 -0
- 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
|
+
# ------------------------------------------------------------------------------
|