Skip to content

Get SSL Context fail when both cert and key are in the same file #116

Description

@henriberisha

Hello Team,

Great work on this project. I have encountered an issue starting from V7.5.2 and still persistent in latest V7.6.1

When configured to have both cert and key in the same file, it raises a value error in client_functions.py in get_ssl_ctx method.

Before i used this configuration with no problem at all.

Configuration

config["COT_URL"] = "ssl://tak_server_url:8089"
config["PYTAK_TLS_CLIENT_CERT"] = "./cert_key.pem"  ## both cert and key from user.p12 here 
config["PYTAK_TLS_DONT_CHECK_HOSTNAME"] = "1"
config["PYTAK_TLS_DONT_VERIFY"] = "1"

Error Traceback

Traceback (most recent call last):
File "/home/henri.berisha/anaconda3/envs/pytak-v-7-6-1/lib/python3.14/site-packages/pytak/client_functions.py", line 1012, in get_ssl_ctx
    if not os.path.exists(client_cert) or not os.path.exists(client_key):
                                              ~~~~~~~~~~~~~~^^^^^^^^^^^^
  File "<frozen genericpath>", line 19, in exists
TypeError: stat: path should be string, bytes, os.PathLike or integer, not NoneType

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/henri.berisha/pytak_test/quick_start.py", line 44, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "/home/henri.berisha/anaconda3/envs/pytak-v-7-6-1/lib/python3.14/asyncio/runners.py", line 205, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/home/henri.berisha/anaconda3/envs/pytak-v-7-6-1/lib/python3.14/asyncio/runners.py", line 128, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/home/henri.berisha/anaconda3/envs/pytak-v-7-6-1/lib/python3.14/asyncio/base_events.py", line 720, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "/home/henri.berisha/pytak_test/quick_start.py", line 39, in main
    await clitool.setup()
  File "/home/henri.berisha/anaconda3/envs/pytak-v-7-6-1/lib/python3.14/site-packages/pytak/classes.py", line 929, in setup
    write_worker, read_worker = await _make_workers(
                                ^^^^^^^^^^^^^^^^^^^^
        self.tx_queue, self.rx_queue, self.config
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/home/henri.berisha/anaconda3/envs/pytak-v-7-6-1/lib/python3.14/site-packages/pytak/classes.py", line 848, in _make_workers
    reader, writer = await pytak.protocol_factory(config)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/henri.berisha/anaconda3/envs/pytak-v-7-6-1/lib/python3.14/site-packages/pytak/client_functions.py", line 607, in protocol_factory
    reader, writer = await create_tls_client(config, cot_url)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/henri.berisha/anaconda3/envs/pytak-v-7-6-1/lib/python3.14/site-packages/pytak/client_functions.py", line 934, in create_tls_client
    ssl_ctx = get_ssl_ctx(tls_config)
  File "/home/henri.berisha/anaconda3/envs/pytak-v-7-6-1/lib/python3.14/site-packages/pytak/client_functions.py", line 1020, in get_ssl_ctx
    raise ValueError(
    ...<3 lines>...
    ) from exc
ValueError: Error opening resource. Using: PYTAK_TLS_CLIENT_CERT=./cert_key.pem [PYTAK_TLS_CLIENT_KEY=None] Using Password: False?

Solution Fix - This mod in get_ssl_ctx fixes the issue when both cert and key are in the same file

def get_ssl_ctx(tls_config: SectionProxy) -> ssl.SSLContext:
    """Configure a TLS socket context."""

    client_cert = tls_config.get("PYTAK_TLS_CLIENT_CERT")
    client_key = tls_config.get("PYTAK_TLS_CLIENT_KEY", fallback=None)
    client_cafile = tls_config.get("PYTAK_TLS_CLIENT_CAFILE", fallback=None)
    client_password = tls_config.get(
        "PYTAK_TLS_CERT_ENROLLMENT_PASSPHRASE",
        fallback=tls_config.get("PYTAK_TLS_CLIENT_PASSWORD", fallback=None),
    )

    client_ciphers = tls_config.get("PYTAK_TLS_CLIENT_CIPHERS") or "ALL"

    # Do not verify CA against our trust store.
    dont_verify = tls_config.getboolean(
        "PYTAK_TLS_DONT_VERIFY",
        fallback=False,
    )

    dont_check_hostname = dont_verify or tls_config.getboolean(
        "PYTAK_TLS_DONT_CHECK_HOSTNAME",
        fallback=False,
    )

    # Client certificate is always required.
    if client_cert:
        if not os.path.exists(client_cert):
            raise SyntaxError(
                f"Resource not found: PYTAK_TLS_CLIENT_CERT={client_cert}"
            )
    else:
        raise SyntaxError("Missing value: PYTAK_TLS_CLIENT_CERT")

    # Client key is optional because the certificate PEM may contain
    # both the certificate and the private key.
    if client_key:
        if not os.path.exists(client_key):
            raise SyntaxError(
                f"Resource not found: PYTAK_TLS_CLIENT_KEY={client_key}"
            )

    # SSL Context
    ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

    ssl_ctx.options |= ssl.OP_NO_TLSv1
    ssl_ctx.options |= ssl.OP_NO_TLSv1_1

    ssl_ctx.set_ciphers(client_ciphers)

    # Checks & Verifications
    ssl_ctx.check_hostname = True
    ssl_ctx.verify_mode = ssl.VerifyMode.CERT_REQUIRED

    # PKCS#12.
    #
    # OpenSSL's load_cert_chain() requires filesystem paths, but the
    # extracted PEMs are needed only for that call. Keeping them in /tmp
    # for the life of a gateway leaks three files on every reconnect and
    # can fill a RAM-backed tmpfs during a long TAK Server outage.
    temporary_cert_paths = []

    if client_cert.endswith(".p12"):
        cert_paths = convert_cert(client_cert, client_password)

        temporary_cert_paths = [
            path for path in cert_paths.values()
            if path
        ]

        client_cert = cert_paths["cert_pem_path"]
        client_key = cert_paths["pk_pem_path"]

    try:
        # The client certificate must exist.
        # Here the client _cert can contain both the cert and key if provided in one file:
        # PYTAK_TLS_CLIENT_CERT = /some/path_to/client_cert_and_key.pem
        if not os.path.exists(client_cert):
            raise SystemError(
                f"Missing client certificate: {client_cert}"
            )

        # If client_key is provided separately, verify that it exists.
        if client_key and not os.path.exists(client_key):
            raise SystemError(
                f"Missing client private key: {client_key}"
            )

        # keyfile=None is intentional.
        #
        # When client_key is None, OpenSSL/Python will look for the
        # private key in the same PEM file as client_cert.
        ssl_ctx.load_cert_chain(
            client_cert,
            keyfile=client_key,
            password=client_password,
        )

    except Exception as exc:
        raise ValueError(
            f"Error opening resource. "
            f"Using: PYTAK_TLS_CLIENT_CERT={client_cert} "
            f"[PYTAK_TLS_CLIENT_KEY={client_key}] "
            f"Using Password: {bool(client_password)}?"
        ) from exc

    finally:
        # Remove temporary PEM files generated from PKCS#12.
        for path in temporary_cert_paths:
            try:
                os.remove(path)
            except FileNotFoundError:
                pass

    # CA File
    if client_cafile:
        ssl_ctx.load_verify_locations(
            cafile=client_cafile
        )

    # Disables TLS Server Common Name Verification
    if dont_check_hostname:
        warnings.warn(
            "Disabled TLS Server Common Name Verification"
        )
        ssl_ctx.check_hostname = False

    # Disables TLS Server Certificate Verification
    if dont_verify:
        warnings.warn(
            "Disabled TLS Server Certificate Verification"
        )
        ssl_ctx.verify_mode = ssl.CERT_NONE

    return ssl_ctx

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions