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/convert.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# stdlib
|
|
2
|
+
import base64
|
|
3
|
+
import binascii
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import subprocess
|
|
7
|
+
import textwrap
|
|
8
|
+
from typing import List
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
# pypi
|
|
12
|
+
import psutil
|
|
13
|
+
|
|
14
|
+
# locals
|
|
15
|
+
from . import conditionals
|
|
16
|
+
from .core import check_openssl_version
|
|
17
|
+
from .core import openssl_path
|
|
18
|
+
from .core import openssl_version
|
|
19
|
+
from .core import validate_key
|
|
20
|
+
from .errors import OpenSslError_InvalidCertificate
|
|
21
|
+
from .utils import cleanup_pem_text
|
|
22
|
+
from .utils import new_der_tempfile
|
|
23
|
+
from .utils import new_pem_tempfile
|
|
24
|
+
from .utils import split_pem_chain
|
|
25
|
+
|
|
26
|
+
# ==============================================================================
|
|
27
|
+
|
|
28
|
+
log = logging.getLogger("cert_utils")
|
|
29
|
+
log.setLevel(logging.INFO)
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def convert_der_to_pem(der_data: bytes) -> str:
|
|
35
|
+
"""
|
|
36
|
+
:param der_data: DER encoded string
|
|
37
|
+
:type der_data: str
|
|
38
|
+
:returns: PEM encoded version of the DER Certificate
|
|
39
|
+
:rtype: str
|
|
40
|
+
"""
|
|
41
|
+
# PEM is just a b64 encoded DER Certificate with the header/footer
|
|
42
|
+
as_pem = """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
|
|
43
|
+
"\n".join(textwrap.wrap(base64.b64encode(der_data).decode("utf8"), 64))
|
|
44
|
+
)
|
|
45
|
+
return as_pem
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def convert_der_to_pem__csr(der_data: bytes) -> str:
|
|
49
|
+
"""
|
|
50
|
+
:param der_data: CSR in DER encoding
|
|
51
|
+
:type der_data: str
|
|
52
|
+
:returns: PEM encoded version of the DER encoded CSR
|
|
53
|
+
:rtype: str
|
|
54
|
+
"""
|
|
55
|
+
# PEM is just a b64 encoded DER CertificateRequest with the header/footer
|
|
56
|
+
as_pem = """-----BEGIN CERTIFICATE REQUEST-----\n{0}\n-----END CERTIFICATE REQUEST-----\n""".format(
|
|
57
|
+
"\n".join(textwrap.wrap(base64.b64encode(der_data).decode("utf8"), 64))
|
|
58
|
+
)
|
|
59
|
+
return as_pem
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def convert_der_to_pem__rsakey(der_data: bytes) -> str:
|
|
63
|
+
"""
|
|
64
|
+
:param der_data: RSA Key in DER encoding
|
|
65
|
+
:type der_data: str
|
|
66
|
+
:returns: PEM encoded version of the RSA Key
|
|
67
|
+
:rtype: str
|
|
68
|
+
|
|
69
|
+
The OpenSSL Equivalent / Fallback is::
|
|
70
|
+
|
|
71
|
+
openssl rsa -in {FILEPATH} -inform der -outform pem
|
|
72
|
+
"""
|
|
73
|
+
# PEM is just a b64 encoded DER RSA KEY with the header/footer
|
|
74
|
+
as_pem = """-----BEGIN RSA PRIVATE KEY-----\n{0}\n-----END RSA PRIVATE KEY-----\n""".format(
|
|
75
|
+
"\n".join(textwrap.wrap(base64.b64encode(der_data).decode("utf8"), 64))
|
|
76
|
+
)
|
|
77
|
+
return as_pem
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def convert_pem_to_der(pem_data: str) -> bytes:
|
|
81
|
+
"""
|
|
82
|
+
:param pem_data: PEM encoded data
|
|
83
|
+
:type pem_data: str
|
|
84
|
+
:returns: DER encoded version of the PEM data
|
|
85
|
+
:rtype: str
|
|
86
|
+
|
|
87
|
+
The OpenSSL Equivalent / Fallback is::
|
|
88
|
+
|
|
89
|
+
openssl req -in {FILEPATH} -outform DER
|
|
90
|
+
|
|
91
|
+
The RFC requires the PEM header/footer to start/end with 5 dashes
|
|
92
|
+
This function is a bit lazy and does not check that.
|
|
93
|
+
"""
|
|
94
|
+
# PEM is just a b64 encoded DER data with the appropiate header/footer
|
|
95
|
+
lines = [_l.strip() for _l in pem_data.strip().split("\n")]
|
|
96
|
+
# remove the BEGIN CERT
|
|
97
|
+
if (
|
|
98
|
+
("BEGIN CERTIFICATE" in lines[0])
|
|
99
|
+
or ("BEGIN RSA PRIVATE KEY" in lines[0])
|
|
100
|
+
or ("BEGIN PRIVATE KEY" in lines[0])
|
|
101
|
+
or ("BEGIN CERTIFICATE REQUEST" in lines[0])
|
|
102
|
+
):
|
|
103
|
+
lines = lines[1:]
|
|
104
|
+
if (
|
|
105
|
+
("END CERTIFICATE" in lines[-1])
|
|
106
|
+
or ("END RSA PRIVATE KEY" in lines[-1])
|
|
107
|
+
or ("END PRIVATE KEY" in lines[-1])
|
|
108
|
+
or ("END CERTIFICATE REQUEST" in lines[-1])
|
|
109
|
+
):
|
|
110
|
+
lines = lines[:-1]
|
|
111
|
+
stringed = "".join(lines)
|
|
112
|
+
result = base64.b64decode(stringed)
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def convert_pkcs7_to_pems(pkcs7_data: bytes) -> List[str]:
|
|
117
|
+
"""
|
|
118
|
+
:param pkcs7_data: pkcs7 encoded Certifcate Chain
|
|
119
|
+
:type pem_data: str
|
|
120
|
+
:returns: list of PEM encoded Certificates in the pkcs7_data
|
|
121
|
+
:rtype: list
|
|
122
|
+
|
|
123
|
+
The OpenSSL Equivalent / Fallback is::
|
|
124
|
+
|
|
125
|
+
openssl pkcs7 -inform DER -in {FILEPATH} -print_certs -outform PEM
|
|
126
|
+
"""
|
|
127
|
+
# TODO: accept a pkcs7 filepath; FallbackError_FilepathRequired
|
|
128
|
+
log.info("convert_pkcs7_to_pems >")
|
|
129
|
+
if conditionals.cryptography:
|
|
130
|
+
if TYPE_CHECKING:
|
|
131
|
+
assert conditionals.crypto_pkcs7 is not None
|
|
132
|
+
assert conditionals.crypto_serialization is not None
|
|
133
|
+
certs_loaded = conditionals.crypto_pkcs7.load_der_pkcs7_certificates(pkcs7_data)
|
|
134
|
+
certs_bytes = [
|
|
135
|
+
cert.public_bytes(conditionals.crypto_serialization.Encoding.PEM)
|
|
136
|
+
for cert in certs_loaded
|
|
137
|
+
]
|
|
138
|
+
certs_string = [cert.decode("utf8") for cert in certs_bytes]
|
|
139
|
+
certs_string = [cleanup_pem_text(cert) for cert in certs_string]
|
|
140
|
+
return certs_string
|
|
141
|
+
|
|
142
|
+
log.debug(".convert_pkcs7_to_pems > openssl fallback")
|
|
143
|
+
if openssl_version is None:
|
|
144
|
+
check_openssl_version()
|
|
145
|
+
|
|
146
|
+
_tmpfile_der = new_der_tempfile(pkcs7_data)
|
|
147
|
+
try:
|
|
148
|
+
cert_der_filepath = _tmpfile_der.name
|
|
149
|
+
with psutil.Popen(
|
|
150
|
+
[
|
|
151
|
+
openssl_path,
|
|
152
|
+
"pkcs7",
|
|
153
|
+
"-inform",
|
|
154
|
+
"DER",
|
|
155
|
+
"-in",
|
|
156
|
+
cert_der_filepath,
|
|
157
|
+
"-print_certs",
|
|
158
|
+
"-outform",
|
|
159
|
+
"PEM",
|
|
160
|
+
],
|
|
161
|
+
stdout=subprocess.PIPE,
|
|
162
|
+
stderr=subprocess.PIPE,
|
|
163
|
+
) as proc:
|
|
164
|
+
data_bytes, err = proc.communicate()
|
|
165
|
+
if not data_bytes:
|
|
166
|
+
raise OpenSslError_InvalidCertificate(err)
|
|
167
|
+
data_str = data_bytes.decode()
|
|
168
|
+
# OpenSSL might return extra info
|
|
169
|
+
# for example: "subject=/O=Digital Signature Trust Co./CN=DST Root CA X3\nissuer=/O=Digital Signature Trust Co./CN=DST Root CA X3\n-----BEGIN CERTIFICATE---[...]"
|
|
170
|
+
# split_pem_chain works perfectly with this payload!
|
|
171
|
+
certs = split_pem_chain(data_str)
|
|
172
|
+
return certs
|
|
173
|
+
except Exception as exc: # noqa: F841
|
|
174
|
+
raise
|
|
175
|
+
finally:
|
|
176
|
+
_tmpfile_der.close()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def convert_jwk_to_ans1(pkey_jsons: str) -> str:
|
|
180
|
+
"""
|
|
181
|
+
input is a json string
|
|
182
|
+
|
|
183
|
+
adapted from https://github.com/JonLundy
|
|
184
|
+
who shared this gist under the MIT license:
|
|
185
|
+
https://gist.github.com/JonLundy/f25c99ee0770e19dc595
|
|
186
|
+
|
|
187
|
+
:param pkey_jsons: JWK Key
|
|
188
|
+
:type pkey_jsons: str
|
|
189
|
+
:returns: Key in ANS1 Format
|
|
190
|
+
:rtype: str
|
|
191
|
+
"""
|
|
192
|
+
pkey = json.loads(pkey_jsons)
|
|
193
|
+
|
|
194
|
+
def enc(data_bytes: bytes) -> str:
|
|
195
|
+
missing_padding = 4 - len(data_bytes) % 4
|
|
196
|
+
if missing_padding:
|
|
197
|
+
data_bytes += b"=" * missing_padding
|
|
198
|
+
data_bytes = binascii.hexlify(base64.b64decode(data_bytes, b"-_")).upper()
|
|
199
|
+
data_str = data_bytes.decode("utf8")
|
|
200
|
+
return "0x" + data_str
|
|
201
|
+
|
|
202
|
+
for k, v in list(pkey.items()):
|
|
203
|
+
if k == "kty":
|
|
204
|
+
continue
|
|
205
|
+
pkey[k] = enc(v.encode())
|
|
206
|
+
|
|
207
|
+
converted = []
|
|
208
|
+
converted.append("asn1=SEQUENCE:private_key\n[private_key]\nversion=INTEGER:0")
|
|
209
|
+
converted.append("n=INTEGER:{}".format(pkey["n"]))
|
|
210
|
+
converted.append("e=INTEGER:{}".format(pkey["e"]))
|
|
211
|
+
converted.append("d=INTEGER:{}".format(pkey["d"]))
|
|
212
|
+
converted.append("p=INTEGER:{}".format(pkey["p"]))
|
|
213
|
+
converted.append("q=INTEGER:{}".format(pkey["q"]))
|
|
214
|
+
converted.append("dp=INTEGER:{}".format(pkey["dp"]))
|
|
215
|
+
converted.append("dq=INTEGER:{}".format(pkey["dq"]))
|
|
216
|
+
converted.append("qi=INTEGER:{}".format(pkey["qi"]))
|
|
217
|
+
converted.append("") # trailing newline
|
|
218
|
+
converted_ = "\n".join(converted)
|
|
219
|
+
return converted_
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def convert_lejson_to_pem(pkey_jsons: str) -> str:
|
|
223
|
+
"""
|
|
224
|
+
This routine will use crypto/certbot if available.
|
|
225
|
+
If not, openssl is used via subprocesses
|
|
226
|
+
|
|
227
|
+
input is a json string
|
|
228
|
+
|
|
229
|
+
adapted from https://github.com/JonLundy
|
|
230
|
+
who shared this gist under the MIT license:
|
|
231
|
+
https://gist.github.com/JonLundy/f25c99ee0770e19dc595
|
|
232
|
+
|
|
233
|
+
openssl asn1parse -noout -out private_key.der -genconf <(python conv.py private_key.json)
|
|
234
|
+
openssl rsa -in private_key.der -inform der > private_key.pem
|
|
235
|
+
openssl rsa -in private_key.pem
|
|
236
|
+
|
|
237
|
+
:param pkey_jsons: LetsEncrypt JSON formatted Key
|
|
238
|
+
:type pkey_jsons: str
|
|
239
|
+
:returns: Key in PEM Encoding
|
|
240
|
+
:rtype: str
|
|
241
|
+
"""
|
|
242
|
+
log.info("convert_lejson_to_pem >")
|
|
243
|
+
|
|
244
|
+
if conditionals.cryptography and conditionals.josepy:
|
|
245
|
+
if TYPE_CHECKING:
|
|
246
|
+
assert conditionals.crypto_serialization is not None
|
|
247
|
+
pkey = conditionals.josepy.JWKRSA.json_loads(pkey_jsons)
|
|
248
|
+
as_pem = pkey.key.private_bytes(
|
|
249
|
+
encoding=conditionals.crypto_serialization.Encoding.PEM,
|
|
250
|
+
format=conditionals.crypto_serialization.PrivateFormat.TraditionalOpenSSL,
|
|
251
|
+
encryption_algorithm=conditionals.crypto_serialization.NoEncryption(),
|
|
252
|
+
)
|
|
253
|
+
as_pem = as_pem.decode("utf8")
|
|
254
|
+
as_pem = cleanup_pem_text(as_pem)
|
|
255
|
+
|
|
256
|
+
# note: we don't need to provide key_pem_filepath because we already rely on openssl
|
|
257
|
+
key_technology = validate_key(key_pem=as_pem)
|
|
258
|
+
return as_pem
|
|
259
|
+
|
|
260
|
+
log.debug(".convert_lejson_to_pem > openssl fallback")
|
|
261
|
+
if openssl_version is None:
|
|
262
|
+
check_openssl_version()
|
|
263
|
+
|
|
264
|
+
pkey_ans1 = convert_jwk_to_ans1(pkey_jsons)
|
|
265
|
+
as_pem = None
|
|
266
|
+
tmpfiles = []
|
|
267
|
+
try:
|
|
268
|
+
tmpfile_ans1 = new_pem_tempfile(pkey_ans1)
|
|
269
|
+
tmpfiles.append(tmpfile_ans1)
|
|
270
|
+
|
|
271
|
+
tmpfile_der = new_pem_tempfile("")
|
|
272
|
+
tmpfiles.append(tmpfile_der)
|
|
273
|
+
|
|
274
|
+
with psutil.Popen(
|
|
275
|
+
[
|
|
276
|
+
openssl_path,
|
|
277
|
+
"asn1parse",
|
|
278
|
+
"-noout",
|
|
279
|
+
"-out",
|
|
280
|
+
tmpfile_der.name,
|
|
281
|
+
"-genconf",
|
|
282
|
+
tmpfile_ans1.name,
|
|
283
|
+
],
|
|
284
|
+
stdout=subprocess.PIPE,
|
|
285
|
+
stderr=subprocess.PIPE,
|
|
286
|
+
) as proc:
|
|
287
|
+
generated, err = proc.communicate()
|
|
288
|
+
if err:
|
|
289
|
+
raise ValueError(err)
|
|
290
|
+
# convert to pem
|
|
291
|
+
as_pem = convert_der_to_pem__rsakey(tmpfile_der.read())
|
|
292
|
+
|
|
293
|
+
# we need a tmpfile to validate it
|
|
294
|
+
tmpfile_pem = new_pem_tempfile(as_pem)
|
|
295
|
+
tmpfiles.append(tmpfile_pem)
|
|
296
|
+
|
|
297
|
+
# validate it
|
|
298
|
+
key_technology = validate_key( # noqa: F841
|
|
299
|
+
key_pem=as_pem, key_pem_filepath=tmpfile_pem.name
|
|
300
|
+
)
|
|
301
|
+
return as_pem
|
|
302
|
+
|
|
303
|
+
except Exception as exc: # noqa: F841
|
|
304
|
+
raise
|
|
305
|
+
finally:
|
|
306
|
+
for t in tmpfiles:
|
|
307
|
+
t.close()
|