|
1 | 1 | # Copyright 2020-2024 The MathWorks, Inc. |
2 | 2 |
|
| 3 | +import datetime |
3 | 4 | import os |
4 | 5 | import shutil |
5 | 6 | import socket |
|
9 | 10 | import xml.etree.ElementTree as ET |
10 | 11 | from pathlib import Path |
11 | 12 |
|
| 13 | +from cryptography import x509 |
| 14 | +from cryptography.hazmat.primitives import hashes, serialization |
| 15 | +from cryptography.hazmat.primitives.asymmetric import rsa |
| 16 | +from cryptography.x509.oid import NameOID |
| 17 | + |
12 | 18 | import matlab_proxy |
13 | 19 | from matlab_proxy import constants |
14 | 20 | from matlab_proxy.util import mwi, system |
@@ -289,10 +295,6 @@ def get_server_settings(config_name): |
289 | 295 | mwi_auth_token_hash, |
290 | 296 | ) = token_auth.generate_mwi_auth_token_and_hash().values() |
291 | 297 | mwi_config_folder = get_mwi_config_folder() |
292 | | - ssl_key_file, ssl_cert_file = mwi.validators.validate_ssl_key_and_cert_file( |
293 | | - os.getenv(mwi_env.get_env_name_ssl_key_file(), None), |
294 | | - os.getenv(mwi_env.get_env_name_ssl_cert_file(), None), |
295 | | - ) |
296 | 298 |
|
297 | 299 | # log file validation check is already done in logger.py |
298 | 300 | mwi_log_file = os.getenv(mwi_env.get_env_name_log_file(), None) |
@@ -328,9 +330,7 @@ def get_server_settings(config_name): |
328 | 330 | "mwi_use_existing_license": mwi.validators.validate_use_existing_licensing( |
329 | 331 | os.getenv(mwi_env.get_env_name_mwi_use_existing_license(), "") |
330 | 332 | ), |
331 | | - "ssl_context": get_ssl_context( |
332 | | - ssl_cert_file=ssl_cert_file, ssl_key_file=ssl_key_file |
333 | | - ), |
| 333 | + "ssl_context": _validate_ssl_files_and_get_ssl_context(mwi_config_folder), |
334 | 334 | } |
335 | 335 |
|
336 | 336 |
|
@@ -478,31 +478,127 @@ def get_test_temp_dir(): |
478 | 478 | return test_temp_dir |
479 | 479 |
|
480 | 480 |
|
481 | | -def get_ssl_context(ssl_cert_file, ssl_key_file): |
482 | | - """Creates an SSL CONTEXT for use with the TCP Site""" |
| 481 | +def _validate_ssl_files_and_get_ssl_context(mwi_config_folder): |
| 482 | + """Creates an SSL CONTEXT for use with the TCP Site. |
| 483 | + The certfile string must be the path to a single file in PEM format containing the |
| 484 | + certificate as well as any number of CA certificates needed to establish the certificate’s authenticity. |
| 485 | + The keyfile string, if present, must point to a file containing the private key in. |
| 486 | + Otherwise the private key will be taken from certfile as well. |
| 487 | + """ |
| 488 | + is_self_signed_certificates = False |
| 489 | + env_name_enable_ssl = mwi_env.get_env_name_enable_ssl() |
| 490 | + is_ssl_enabled = mwi_env._is_env_set_to_true(env_name_enable_ssl) |
| 491 | + env_name_ssl_key_file = mwi_env.get_env_name_ssl_key_file() |
| 492 | + env_name_ssl_cert_file = mwi_env.get_env_name_ssl_cert_file() |
| 493 | + |
| 494 | + ssl_key_file, ssl_cert_file = ( |
| 495 | + os.getenv(env_name_ssl_key_file, None), |
| 496 | + os.getenv(env_name_ssl_cert_file, None), |
| 497 | + ) |
483 | 498 |
|
484 | | - # The certfile string must be the path to a single file in PEM format containing the |
485 | | - # certificate as well as any number of CA certificates needed to establish the certificate’s authenticity. |
486 | | - # The keyfile string, if present, must point to a file containing the private key in. |
487 | | - # Otherwise the private key will be taken from certfile as well. |
488 | | - import traceback |
| 499 | + # Don't use SSL if the user has explicitly disabled SSL communication or not set the respective env var |
| 500 | + if not is_ssl_enabled: |
| 501 | + if ssl_cert_file: |
| 502 | + logger.warn( |
| 503 | + f"Ignoring provided SSL files, as {env_name_enable_ssl} is either unset or set to false" |
| 504 | + ) |
| 505 | + return None |
489 | 506 |
|
490 | | - if ssl_cert_file != None: |
491 | | - try: |
492 | | - ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) |
493 | | - ssl_context.load_cert_chain(ssl_cert_file, ssl_key_file) |
494 | | - logger.debug(f"Using SSL certification!") |
495 | | - except Exception as e: |
496 | | - # Something was wrong with the certificates provided |
497 | | - error_message = "SSL certificates provided are invalid. Aborting..." |
498 | | - logger.error(error_message) |
499 | | - traceback.print_exc() |
500 | | - logger.info("==== Fatal error : ===") |
501 | | - print(e) |
502 | | - # printing stack trace |
503 | | - logger.info("======================") |
504 | | - raise FatalError(error_message) |
505 | | - else: |
| 507 | + # Validate that provided SSL files are valid files |
| 508 | + ssl_key_file, ssl_cert_file = mwi.validators.validate_ssl_key_and_cert_file( |
| 509 | + ssl_key_file, ssl_cert_file |
| 510 | + ) |
| 511 | + |
| 512 | + if not ssl_cert_file and not ssl_key_file: |
| 513 | + logger.debug("Using auto-generated self-signed certificates") |
| 514 | + |
| 515 | + # certs dir under the MWI_CONFIG_FOLDER will hold the self-signed certificates |
| 516 | + mwi_certs_dir = mwi_config_folder / "certs" |
| 517 | + mwi_certs_dir.mkdir(parents=True, exist_ok=True) |
| 518 | + |
| 519 | + # New certs are generated for every run leading to functionally reliable system, alternative is |
| 520 | + # to check for existing certs and have error handling around expired/bad certs. |
| 521 | + ssl_cert_file, ssl_key_file = generate_new_self_signed_certs(mwi_certs_dir) |
| 522 | + is_self_signed_certificates = True |
| 523 | + try: |
| 524 | + ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) |
| 525 | + ssl_context.load_cert_chain(ssl_cert_file, ssl_key_file) |
| 526 | + logger.debug("Certificate chain was correctly loaded") |
| 527 | + except Exception as e: |
| 528 | + logger.error(f"Unable to load certificates. Error: {e}") |
| 529 | + |
| 530 | + # Setting to None to use http mode in the event of failing to setup self-signed certificates |
506 | 531 | ssl_context = None |
507 | 532 |
|
| 533 | + # Raise a fatal error only in the event of an exception while loading customer-supplied ssl files |
| 534 | + if not is_self_signed_certificates: |
| 535 | + raise FatalError(e) |
| 536 | + |
508 | 537 | return ssl_context |
| 538 | + |
| 539 | + |
| 540 | +def generate_new_self_signed_certs(mwi_certs_dir): |
| 541 | + """ |
| 542 | + Generates a new self-signed certificate and corresponding private key, saves them as PEM files in the specified directory. |
| 543 | + The certificate is valid for 365 days from the time of creation. |
| 544 | +
|
| 545 | + Parameters: |
| 546 | + - mwi_certs_dir (Path): A pathlib.Path object representing the directory where the certificate and key files will be saved. |
| 547 | +
|
| 548 | + Returns: |
| 549 | + - tuple: A tuple containing the file paths (as strings) to the newly created certificate and private key PEM files. |
| 550 | + The first element is the path to the certificate file (cert.pem), and the second is the path to the key file (key.pem). |
| 551 | +
|
| 552 | + Raises: |
| 553 | + - FileNotFoundError: If the mwi_certs_dir does not exist. |
| 554 | + - Any other exception that may occur during file writing or certificate generation. |
| 555 | + """ |
| 556 | + cert_file = priv_key_file = None |
| 557 | + try: |
| 558 | + # Generate private key |
| 559 | + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) |
| 560 | + |
| 561 | + # Self-signed certificate |
| 562 | + subject = issuer = x509.Name( |
| 563 | + [ |
| 564 | + x509.NameAttribute(NameOID.COUNTRY_NAME, "US"), |
| 565 | + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Massachusetts"), |
| 566 | + x509.NameAttribute(NameOID.LOCALITY_NAME, "Natick"), |
| 567 | + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MathWorks Inc."), |
| 568 | + x509.NameAttribute(NameOID.COMMON_NAME, "mathworks.com"), |
| 569 | + ] |
| 570 | + ) |
| 571 | + cert = ( |
| 572 | + x509.CertificateBuilder() |
| 573 | + .subject_name(subject) |
| 574 | + .issuer_name(issuer) |
| 575 | + .public_key(private_key.public_key()) |
| 576 | + .serial_number(x509.random_serial_number()) |
| 577 | + .not_valid_before(datetime.datetime.utcnow()) |
| 578 | + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365)) |
| 579 | + .sign(private_key, hashes.SHA256()) |
| 580 | + ) |
| 581 | + |
| 582 | + # Write private key to file |
| 583 | + priv_key_file = mwi_certs_dir / "key.pem" |
| 584 | + with open(priv_key_file, "wb") as f: |
| 585 | + f.write( |
| 586 | + private_key.private_bytes( |
| 587 | + encoding=serialization.Encoding.PEM, |
| 588 | + format=serialization.PrivateFormat.TraditionalOpenSSL, |
| 589 | + encryption_algorithm=serialization.NoEncryption(), |
| 590 | + ) |
| 591 | + ) |
| 592 | + |
| 593 | + # Write certificate to file |
| 594 | + cert_file = mwi_certs_dir / "cert.pem" |
| 595 | + with open(cert_file, "wb") as f: |
| 596 | + f.write(cert.public_bytes(serialization.Encoding.PEM)) |
| 597 | + |
| 598 | + except Exception as ex: |
| 599 | + logger.warn( |
| 600 | + f"Failed to generate self-signed certificates, proceeding with non-secure mode! Error: {ex}" |
| 601 | + ) |
| 602 | + cert_file = priv_key_file = None |
| 603 | + |
| 604 | + return cert_file, priv_key_file |
0 commit comments