forked from drGrove/mtls-server
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cert_processor.py
505 lines (447 loc) · 18.6 KB
/
cert_processor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
"""Certificate Processor."""
import datetime
import os
import uuid
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
import gnupg
from key_refresh import KeyRefresh
from logger import logger
from storage import StorageEngine
from storage import StorageEngineCertificateConflict
from storage import StorageEngineMissing
from storage import UpdateCertException
from utils import create_dir_if_missing
class CertProcessorKeyNotFoundError(Exception):
pass
class CertProcessorInvalidSignatureError(Exception):
pass
class CertProcessorUntrustedSignatureError(Exception):
pass
class CertProcessorMismatchedPublicKeyError(Exception):
pass
class CertProcessorNotAdminUserError(Exception):
pass
class CertProcessorNoPGPKeyFoundError(Exception):
pass
class CertProcessor:
def __init__(self, config):
"""Cerificate Processor.
Args:
config (ConfigParser): a config as configparser.
"""
user_gnupg_path = config.get("gnupg", "user")
admin_gnupg_path = config.get("gnupg", "admin")
if not os.path.isabs(user_gnupg_path):
user_gnupg_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), user_gnupg_path)
)
if not os.path.isabs(admin_gnupg_path):
admin_gnupg_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), admin_gnupg_path)
)
create_dir_if_missing(user_gnupg_path)
create_dir_if_missing(admin_gnupg_path)
self.user_gpg = gnupg.GPG(gnupghome=user_gnupg_path)
self.admin_gpg = gnupg.GPG(gnupghome=admin_gnupg_path)
self.user_gpg.encoding = "utf-8"
self.admin_gpg.encoding = "utf-8"
# Start Background threads for getting revoke/exipry from Keyserver
user_key_refesh = KeyRefresh('user_key_refresh', self.user_gpg, config)
admin_key_refresh = KeyRefresh('admin_key_refresh', self.admin_gpg, config)
if config.get("storage", "engine", fallback=None) is None:
raise StorageEngineMissing()
self.storage = StorageEngine(config)
self.storage.init_db()
self.config = config
self.openssl_format = serialization.PrivateFormat.TraditionalOpenSSL
self.no_encyption = serialization.NoEncryption()
self.SERVER_URL = os.environ.get("FQDN") or "localhost"
self.PROTOCOL = os.environ.get("PROTOCOL") or "http"
def verify(self, data, signature):
"""Verifies that the signed data is signed by a trusted key.
Args:
data (str): The data to be verified.
signature (str): The signature file.
Raises:
CertProcessorInvalidSignatureError: Signing Key not in trust store.
CertProcessorUntrustedSignatureError: Signing Key in trust store
but does not have to correct permissions.
Returns:
str: The fingerprint of the signer.
"""
verified = self.user_gpg.verify_data(signature, data)
if verified is None:
logger.error("Invalid signature")
raise CertProcessorInvalidSignatureError
if (
verified.trust_level is not None
and verified.trust_level < verified.TRUST_FULLY
):
logger.error(
"User with fingerprint: {} does not have the required trust".format(
verified.pubkey_fingerprint
)
)
raise CertProcessorUntrustedSignatureError
if verified.valid is None or verified.valid is False:
raise CertProcessorInvalidSignatureError
return verified.pubkey_fingerprint
def admin_verify(self, data, signature):
"""Verifies that the signed data is signed by an admin key.
Args:
data (str): The data to be verified
signature (str): The signature file
Raises:
CertProcessorInvalidSignatureError: Signing Key not in trust store.
CertProcessorUntrustedSignatureError: Signing Key in trust store
but does not have to correct permissions.
Returns:
str: The fingerprint of the signer.
"""
verified = self.admin_gpg.verify_data(signature, data)
if verified is None:
raise CertProcessorInvalidSignatureError
if verified.valid is None or verified.valid is False:
logger.error("Invalid signature for {}".format(verified.fingerprint))
raise CertProcessorInvalidSignatureError
if (
verified.trust_level is not None
and verified.trust_level < verified.TRUST_FULLY
):
logger.error(
"User with fingerprint: {} does not have the required trust".format(
verified.pubkey_fingerprint
)
)
raise CertProcessorUntrustedSignatureError
return verified.pubkey_fingerprint
def get_csr(self, csr):
"""Given a CSR string, get a cryptography CSR Object.
Args:
csr (str): A csr string.
Returns:
cryptography.x509.CertificateSigningRequest: A cryptography CSR
Object if it can be parsed, otherwise None.
"""
try:
return x509.load_pem_x509_csr(bytes(csr, "utf-8"), default_backend())
except Exception as e:
logger.error(e)
return None
def get_ca_key(self):
"""Get the CA Key.
Returns:
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey: The
CAs private key.
"""
ca_key_path = self.config.get("ca", "key")
if not os.path.isabs(ca_key_path):
ca_key_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), ca_key_path)
)
try:
ca_dir = "/".join(ca_key_path.split("/")[:-1])
if not os.path.isdir(ca_dir):
os.makedirs(ca_dir)
with open(ca_key_path, "rb") as key_file:
if os.environ.get("CA_KEY_PASSWORD"):
pw = os.environ.get("CA_KEY_PASSWORD").encode("UTF-8")
else:
pw = None
ca_key = serialization.load_pem_private_key(
key_file.read(), password=pw, backend=default_backend()
)
return ca_key
except (ValueError, FileNotFoundError) as e:
logger.error("Error opening file: {}".format(ca_key_path))
logger.info("Generating new root key...")
key = rsa.generate_private_key(
public_exponent=65537, key_size=4096, backend=default_backend()
)
if os.environ.get("CA_KEY_PASSWORD"):
encryption_algorithm = serialization.BestAvailableEncryption(
os.environ.get("CA_KEY_PASSWORD").encode("UTF-8")
)
else:
encryption_algorithm = self.no_encyption
key_data = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=self.openssl_format,
encryption_algorithm=encryption_algorithm,
)
with open(ca_key_path, "wb") as f:
f.write(key_data)
return key
def get_ca_cert(self, key=None):
"""Get the CA Certificate.
Args:
key (cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
A key in cryptography RSAPrivateKey format.
Returns:
cryptography.x509.Certificate: The CA Certificate.
Returns:
cryptography.x509.Certificate: CA Certificate
"""
ca_cert_path = self.config.get("ca", "cert")
if not os.path.isabs(ca_cert_path):
ca_cert_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), ca_cert_path)
)
# Grab the CA Certificate from filesystem if it exists and return
if os.path.isfile(ca_cert_path):
with open(ca_cert_path, "rb") as cert_file:
ca_cert = x509.load_pem_x509_certificate(
cert_file.read(), default_backend()
)
return ca_cert
if key is None:
raise CertProcessorKeyNotFoundError()
key_id = x509.SubjectKeyIdentifier.from_public_key(key.public_key())
subject = issuer = x509.Name(
[x509.NameAttribute(NameOID.COMMON_NAME, self.config.get("ca", "issuer"))]
)
now = datetime.datetime.utcnow()
serial = x509.random_serial_number()
ca_cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(key.public_key())
.serial_number(serial)
.not_valid_before(now)
.not_valid_after(now + datetime.timedelta(days=365))
.add_extension(key_id, critical=False)
.add_extension(
x509.AuthorityKeyIdentifier(
key_id.digest, [x509.DirectoryName(issuer)], serial
),
critical=False,
)
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=True,
crl_sign=True,
encipher_only=False,
decipher_only=False,
),
critical=True,
)
.sign(key, hashes.SHA256(), default_backend())
)
with open(ca_cert_path, "wb") as f:
f.write(ca_cert.public_bytes(serialization.Encoding.PEM))
return ca_cert
def is_admin(self, fingerprint):
"""Determine if the fingerprint is associated with with an admin.
Args:
fingerprint (str): The users fingerprint.
Returns:
bool: Is the user an admin.
"""
if self.get_gpg_key_by_fingerprint(fingerprint, True) is not None:
return True
return False
def get_gpg_key_by_fingerprint(self, fingerprint, is_admin=False):
if is_admin:
keys = self.admin_gpg.list_keys()
else:
keys = self.user_gpg.list_keys()
for key in keys:
if key["fingerprint"] == fingerprint:
return key
return None
def check_subject_against_key(self, subj, signer_key):
"""Check a subject email against the signing fingerprint.
The only exception to this is if an admin user is to generate a
certificate on behalf of someone else. This should be done with extreme
care, but access will only be allowed for the life of the certificate.
Args:
subj (cryptography.x509.Name): An x509 subject.
signer_key (dict): PGP key details from python-gnupg.
Returns:
Wheather the subject email matches a PGP uid for a given
fingerprint.
"""
email = subj.get_attributes_for_oid(NameOID.EMAIL_ADDRESS)[0].value
return any(email in uid for uid in signer_key["uids"])
def generate_cert(self, csr, lifetime, fingerprint):
"""Generate a Certificate from a CSR.
Args:
csr: The CSR object
lifetime: The lifetime of the certificate in seconds
fingerprint: The fingerprint of the signer for the CSR.
Raises:
CertProcessorNotAdminUserError: When an admin request is made
without and admin key
CertProcessorInvalidSignatureError: When an invalid user attempts
to sign a request for a certificate
Returns:
The certificates public bytes
"""
ca_pkey = self.get_ca_key()
ca_cert = self.get_ca_cert(ca_pkey)
now = datetime.datetime.utcnow()
lifetime_delta = now + datetime.timedelta(seconds=int(lifetime))
alts = []
is_admin = self.is_admin(fingerprint)
logger.info(f"generate_cert: getting gpg key for {fingerprint}")
user_gpg_key = self.get_gpg_key_by_fingerprint(fingerprint, is_admin)
if user_gpg_key is None:
raise CertProcessorNoPGPKeyFoundError()
email_in_key = self.check_subject_against_key(csr.subject, user_gpg_key)
if not email_in_key and not is_admin:
raise CertProcessorNotAdminUserError()
for alt in self.config.get("ca", "alternate_name").split(","):
alts.append(x509.DNSName(u"{}".format(alt)))
cert = (
x509.CertificateBuilder()
.subject_name(csr.subject)
.issuer_name(ca_cert.subject)
.public_key(csr.public_key())
.serial_number(uuid.uuid4().int)
.not_valid_before(now)
.not_valid_after(lifetime_delta)
)
if len(alts) > 0:
cert = cert.add_extension(x509.SubjectAlternativeName(alts), critical=False)
crl_dp = x509.DistributionPoint(
[
x509.UniformResourceIdentifier(
"{protocol}://{server_url}/crl".format(
protocol=self.PROTOCOL, server_url=self.SERVER_URL
)
)
],
relative_name=None,
reasons=None,
crl_issuer=None,
)
cert = cert.add_extension(x509.CRLDistributionPoints([crl_dp]), critical=False)
logger.info(f"generate_cert: Signing certificate for {fingerprint}")
cert = cert.sign(
private_key=ca_pkey, algorithm=hashes.SHA256(), backend=default_backend()
)
try:
logger.info(f"generate_cert: saving certificate for {fingerprint}")
self.storage.save_cert(cert, fingerprint)
except StorageEngineCertificateConflict:
logger.info(f"generate_cert: updating certificate for {fingerprint}")
cert = self.update_cert(csr, lifetime)
return cert.public_bytes(serialization.Encoding.PEM)
def update_cert(self, csr, lifetime):
"""Given a CSR, look it up in the database, update it and present the
new certificate.
Args:
csr (cryptography.x509.CertificateSigningRequest): A CSR.
lifetime (int): Lifetime in seconds.
Raises:
CertProcessorMismatchedPublicKeyError: The public key from the new
CSR does not match the in database Certificate.
Returns:
cryptography.x509.Certificate: A Signed Certificate for a user.
"""
common_name = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
common_name = common_name[0].value
bcert = bytes(str(self.storage.get_cert(common_name=common_name)[0]), "UTF-8")
old_cert = x509.load_pem_x509_certificate(bcert, backend=default_backend())
old_cert_pub = (
old_cert.public_key()
.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
.decode("UTF-8")
)
csr_pub = (
csr.public_key()
.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
.decode("UTF-8")
)
if old_cert_pub != csr_pub:
raise CertProcessorMismatchedPublicKeyError
ca_pkey = self.get_ca_key()
ca_cert = self.get_ca_cert(ca_pkey)
now = datetime.datetime.utcnow()
lifetime_delta = now + datetime.timedelta(seconds=int(lifetime))
alts = []
for alt in self.config.get("ca", "alternate_name").split(","):
alts.append(x509.DNSName(u"{}".format(alt)))
cert = (
x509.CertificateBuilder()
.subject_name(old_cert.subject)
.issuer_name(ca_cert.subject)
.public_key(csr.public_key())
.serial_number(old_cert.serial_number)
.not_valid_before(old_cert.not_valid_before)
.not_valid_after(lifetime_delta)
)
if len(alts) > 0:
cert = cert.add_extension(x509.SubjectAlternativeName(alts), critical=False)
crl_dp = x509.DistributionPoint(
[
x509.UniformResourceIdentifier(
"{protocol}://{server_url}/crl".format(
protocol=self.PROTOCOL, server_url=self.SERVER_URL
)
)
],
relative_name=None,
reasons=None,
crl_issuer=None,
)
cert = cert.add_extension(x509.CRLDistributionPoints([crl_dp]), critical=False)
cert = cert.sign(
private_key=ca_pkey, algorithm=hashes.SHA256(), backend=default_backend()
)
self.storage.update_cert(cert=cert, serial_number=cert.serial_number)
return cert
def get_crl(self):
"""Generates a Certificate Revocation List.
Returns:
A Certificate Revocation List.
"""
ca_pkey = self.get_ca_key()
ca_cert = self.get_ca_cert(ca_pkey)
crl = (
x509.CertificateRevocationListBuilder()
.issuer_name(ca_cert.subject)
.last_update(datetime.datetime.utcnow())
.next_update(datetime.datetime.utcnow() + datetime.timedelta(minutes=15))
)
for cert in self.storage.get_revoked_certs():
# Convert the string cert into a cryptography cert object
cert = x509.load_pem_x509_certificate(
bytes(str(cert), "UTF-8"), backend=default_backend()
)
# Add the certificate to the CRL
crl = crl.add_revoked_certificate(
x509.RevokedCertificateBuilder()
.serial_number(cert.serial_number)
.revocation_date(datetime.datetime.utcnow())
.build(backend=default_backend())
)
# Sign the CRL
crl = crl.sign(
private_key=ca_pkey, algorithm=hashes.SHA256(), backend=default_backend()
)
return crl
def revoke_cert(self, serial_number):
"""Given a serial number, revoke a certificate.
Args:
serial_number (int): A certificate serial number.
"""
self.storage.revoke_cert(serial_number)