Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## 0.22.0 [unreleased]

### Features

1. [#254](https://github.com/InfluxCommunity/influxdb3-python/pull/254):
- Add support for mTLS in query api.
- Add examples for config mTLS.

### Breaking Changes

1. [#238](https://github.com/InfluxCommunity/influxdb3-python/pull/238): Drop support for Python 3.9. Python 3.10 or newer is now required.
Expand Down
40 changes: 39 additions & 1 deletion examples/core/basic_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ def main() -> None:
query_with_verify_ssl_off(temp_cert_file)
remove_cert(temp_cert_file)

# This is not a completed examples, please change the paths to your real certificates path.
# config_mtls()


def write_and_query_with_explicit_sys_cert():
print("\nwrite and query with typical linux system cert\n")
Expand Down Expand Up @@ -105,11 +108,46 @@ def query_with_verify_ssl_off(cert):
database=DATABASE,
ssl_ca_cert=cert,
verify_ssl=False) as _client:

query = "SELECT * FROM \"escooter\" ORDER BY time DESC"
reader: pyarrow.Table = _client.query(query, mode="")
print_results(reader.to_pylist())


# Please change these paths with your real path to your certificates.
path_client_cert_file = "path/to/cert_file.crt"
path_client_key_file = "path/to/cert_file.key"
path_client_cert_file_pem = "path/to/cert_file.pem"
path_server_cert = "path/to/server_cert.crt"
key_file_password = "password"


def config_mtls():
print("\nConfig tls/mtls for client\n")

# Config mTLS with client send certificate to the server with separated .crt file and .key file.
# `ssl_ca_cert` is optional.
with InfluxDBClient3(token=TOKEN,
host=HOST,
database=DATABASE,
ssl_ca_cert=path_server_cert,
cert_file=path_client_cert_file,
cert_key_file=path_client_key_file,
cert_key_file_password=key_file_password, # Optional
verify_ssl=True) as _client:
result = _client.get_server_version()
print(result)

# Config mTLS with client send a certificate to the server with both certificate and key bundled inside a .pem file.
# `ssl_ca_cert` is optional.
with InfluxDBClient3(token=TOKEN,
host=HOST,
database=DATABASE,
ssl_ca_cert=path_server_cert,
cert_file=path_client_cert_file_pem,
verify_ssl=True) as _client:
result = _client.get_server_version()
print(result)


if __name__ == "__main__":
main()
6 changes: 6 additions & 0 deletions influxdb_client_3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,12 @@ def __init__(
q_opts_builder.disable_grpc_compression(True)
if kw_keys.__contains__('ssl_ca_cert'):
q_opts_builder.root_certs(kwargs.get('ssl_ca_cert', None))
if kw_keys.__contains__('cert_file'):
q_opts_builder.cert_chain(kwargs.get('cert_file', None))
if kw_keys.__contains__('cert_key_file'):
q_opts_builder.private_key(kwargs.get('cert_key_file', None))
Comment thread
NguyenHoangSon96 marked this conversation as resolved.
Resolved
if kw_keys.__contains__('cert_key_password'):
q_opts_builder.private_key_password(kwargs.get('cert_key_password', None))
if kw_keys.__contains__('verify_ssl'):
q_opts_builder.tls_verify(kwargs.get('verify_ssl', True))
if kw_keys.__contains__('proxy'):
Expand Down
79 changes: 70 additions & 9 deletions influxdb_client_3/query/query_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pyarrow.flight import FlightClient, Ticket, FlightCallOptions, FlightStreamReader

from influxdb_client_3.version import USER_AGENT
from influxdb_client_3.write_client.client.util.helpers import read_certs


class QueryApiOptions(object):
Expand All @@ -14,7 +15,9 @@ class QueryApiOptions(object):

Attributes
----------
tls_root_certs (bytes): contents of an SSL root certificate or chain read as bytes
tls_root_certs (bytes): contents of an SSL root certificate or chain read as bytes
cert_chain (bytes): client certificate if using mutual TLS
private_key (bytes): client private key for cert_chain is using mutual TLS
tls_verify (bool): whether to verify SSL certificates or not
proxy (str): URL to a proxy server
flight_client_options (dict): base set of flight client options passed to internal pyarrow.flight.FlightClient
Expand All @@ -24,6 +27,9 @@ class QueryApiOptions(object):
"""
_DEFAULT_TIMEOUT = 300.0
tls_root_certs: bytes = None
cert_chain: bytes = None
private_key: bytes = None
private_key_password: str = None
tls_verify: bool = None
proxy: str = None
flight_client_options: dict = None
Expand All @@ -37,11 +43,17 @@ def __init__(self, root_certs_path: str,
flight_client_options: dict,
timeout: float = _DEFAULT_TIMEOUT,
disable_grpc_compression: bool = False,
middleware: list = None):
middleware: list = None,
cert_chain: str = None,
private_key: str = None,
private_key_password: str = None):
"""
Initialize a set of QueryApiOptions

:param root_certs_path: path to a certificate .pem file.
:param cert_chain: client certificate if using mutual TLS
:param private_key: client private key for cert_chain is using mutual TLS
:param private_key_password: password for private_key if using mutual TLS
:param verify: whether to verify SSL certificates or not.
:param proxy: URL of a proxy server, if required.
:param flight_client_options: set of flight_client_options
Expand All @@ -51,20 +63,21 @@ def __init__(self, root_certs_path: str,
:param middleware: list of middleware functions to be applied to Flight calls.
"""
if root_certs_path:
self.tls_root_certs = self._read_certs(root_certs_path)
self.tls_root_certs = read_certs(root_certs_path)
if cert_chain:
self.cert_chain = read_certs(cert_chain)
if private_key:
if private_key_password:
self.private_key = read_certs(private_key, private_key_password)
else:
self.private_key = read_certs(private_key)
self.tls_verify = verify
self.proxy = proxy
self.flight_client_options = flight_client_options
self.timeout = timeout
self.disable_grpc_compression = disable_grpc_compression
self.middleware = middleware

def _read_certs(self, path: str) -> bytes:
with open(path, "rb") as certs_file:
certs = certs_file.read()
# Normalize PEM line endings so behavior is stable across platforms.
return certs.replace(b"\r\n", b"\n").replace(b"\r", b"\n")


class QueryApiOptionsBuilder(object):
"""
Expand All @@ -77,12 +90,18 @@ class QueryApiOptionsBuilder(object):
options = QueryApiOptionsBuilder()\
.proxy("http://internal.tunnel.proxy:8080") \
.root_certs("/home/fred/.etc/ssl/alt_certs.pem") \
.cert_chain("/home/fred/.etc/ssl/client.crt") \
.private_key("/home/fred/.etc/ssl/client.key") \
.private_key_password("password") \
.tls_verify(True) \
.build()

client = QueryApi(connection, token, None, None, options)
"""
_root_certs_path: str = None
_cert_chain: str = None
_private_key: str = None
_private_key_password: str = None
_tls_verify: bool = True
_proxy: str = None
_flight_client_options: dict = None
Expand All @@ -94,6 +113,18 @@ def root_certs(self, path: str):
self._root_certs_path = path
return self

def cert_chain(self, path: str):
self._cert_chain = path
return self

def private_key(self, path: str):
self._private_key = path
return self

def private_key_password(self, password: str):
self._private_key_password = password
return self

def tls_verify(self, verify: bool):
self._tls_verify = verify
return self
Expand Down Expand Up @@ -123,6 +154,9 @@ def build(self) -> QueryApiOptions:
"""Build a QueryApiOptions object with previously set values"""
return QueryApiOptions(
root_certs_path=self._root_certs_path,
cert_chain=self._cert_chain,
private_key=self._private_key,
private_key_password=self._private_key_password,
verify=self._tls_verify,
proxy=self._proxy,
flight_client_options=self._flight_client_options,
Expand Down Expand Up @@ -180,8 +214,35 @@ def __init__(self,
if ('generic_options' in options.flight_client_options and
'grpc.secondary_user_agent' in dict(options.flight_client_options["generic_options"]).keys()):
self._flight_client_options['generic_options'].remove(default_user_agent)

if options.tls_root_certs:
self._flight_client_options["tls_root_certs"] = options.tls_root_certs
if options.cert_chain:
self._flight_client_options["cert_chain"] = options.cert_chain
if options.private_key:
self._flight_client_options["private_key"] = options.private_key
elif (options.private_key is None and options.cert_chain and
not self._flight_client_options.get("private_key")):
# Only fall back to using cert_chain as private_key if the provided PEM
# content clearly includes a private key block (common in combined PEM files).
pem = options.cert_chain
if isinstance(pem, (bytes, bytearray)):
pem_text = pem.decode("utf-8", errors="ignore")
else:
pem_text = str(pem)
has_private_key = ("BEGIN PRIVATE KEY" in pem_text or
"BEGIN RSA PRIVATE KEY" in pem_text or
"BEGIN EC PRIVATE KEY" in pem_text or
"BEGIN ENCRYPTED PRIVATE KEY" in pem_text
)
if has_private_key:
self._flight_client_options["private_key"] = options.cert_chain
else:
raise ValueError(
"mTLS misconfiguration: 'cert_chain' was provided without a "
"'private_key'. Provide 'private_key' explicitly, or pass a "
"combined PEM that includes a private key block."
)
if options.proxy:
self._proxy = options.proxy
if options.tls_verify is not None:
Expand Down
41 changes: 41 additions & 0 deletions influxdb_client_3/write_client/client/util/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""Functions to share utility across client classes."""
from typing import Optional, Union

from cryptography.hazmat.primitives import serialization


def _is_id(value):
Expand Down Expand Up @@ -47,3 +50,41 @@ def get_org_query_param(org, client, required_id=False):
raise e

return _org


def read_certs(path: str, password: Optional[Union[str, bytes]] = None) -> bytes:
"""
Read PEM certificate or key data from a file and produce a stable byte
representation suitable for downstream cryptographic operations. Encrypted
private key material is decrypted when credentials are provided, and PEM line
endings are normalized so the resulting bytes are consistent across operating
systems.

:param path: Filesystem path to the PEM certificate or private key file.
:param password: Optional password used to decrypt encrypted PEM private key
material.
:return: PEM data as bytes with normalized line endings.
:raises OSError: If the file cannot be opened or read.
:raises ValueError: If encrypted key material cannot be decrypted or the PEM
data is invalid.
:raises TypeError: If the provided password is not accepted by the
cryptographic loader.
:raises cryptography.exceptions.UnsupportedAlgorithm: If the PEM data uses an
unsupported key type or encryption algorithm.
"""
with open(path, "rb") as certs_file:
data = certs_file.read()
if password is not None:
password_bytes = password.encode() if isinstance(password, str) else password
private_key_obj = serialization.load_pem_private_key(
data,
password=password_bytes,
)
data = private_key_obj.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)

# Normalize PEM line endings so behavior is stable across platforms.
return data.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
54 changes: 54 additions & 0 deletions scripts/generate-self-signed-cert.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/bin/bash
#
# The MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

set -euo pipefail

KEY_PASSWORD="changeit"
INFLUXDB_SERVER_NAME="influxdb"
OTHER_SERVER_NAME="other-server"
CERTS_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../tests/data/certificates" && pwd)"

for SERVER_NAME in "$INFLUXDB_SERVER_NAME" "$OTHER_SERVER_NAME"; do
echo "### Generate server key and certificate for $SERVER_NAME..."
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 | openssl pkcs8 -topk8 -nocrypt -out "$CERTS_DIR/${SERVER_NAME}.key"

openssl req -new -x509 -key "$CERTS_DIR/${SERVER_NAME}.key" -out "$CERTS_DIR/${SERVER_NAME}.crt" -days 3650 \
-subj "/CN=localhost/O=Development/C=US" \
-addext "subjectAltName = DNS:localhost,IP:127.0.0.1"

cat "$CERTS_DIR/${SERVER_NAME}.crt" "$CERTS_DIR/${SERVER_NAME}.key" > "$CERTS_DIR/${SERVER_NAME}.pem"

# openssl pkcs12 -export -in "$CERTS_DIR/${SERVER_NAME}.crt" -inkey "$CERTS_DIR/${SERVER_NAME}.key" \
# -out "$CERTS_DIR/${SERVER_NAME}.p12" -name "myalias" -password "pass:$KEY_PASSWORD"
done

echo "### Generate client key..."
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 | openssl pkcs8 -topk8 -nocrypt -out "$CERTS_DIR/client.key"
openssl req -new -x509 -key "$CERTS_DIR/client.key" -out "$CERTS_DIR/client.crt" -days 3650 \
-subj "/CN=test-client/O=Development/C=US"
openssl rsa -in "$CERTS_DIR/client.key" -aes256 -passout "pass:$KEY_PASSWORD" -out "$CERTS_DIR/client-encrypted.key"

cat "$CERTS_DIR/client.crt" "$CERTS_DIR/client.key" > "$CERTS_DIR/client.pem"

#openssl pkcs12 -export -in "$CERTS_DIR/client.crt" -inkey "$CERTS_DIR/client.key" \
# -out "$CERTS_DIR/client.p12" -name "myalias" -password "pass:$KEY_PASSWORD"
30 changes: 30 additions & 0 deletions tests/data/certificates/client-encrypted.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQUd5dtLfUMWQE3Ihc
4e1KDAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEPHP2ds2/jQ1AWI4
FKeI3uEEggTQ5aG2Z0LeB4Gd6939NiSd5KYt7e3gtrn4wSWTGlLzBqRX/OrbxiYi
HnowTMleYrTxG7sLTfSqueyw5XARYIkhgcauM6M3XZU5fb5vL+8229hNO5POlxHG
7F5cW7qF2py+8izvqyMI8KNGHXgGpI3xe5/C2GYWLTzcjcIHi6iAk/riKR8Yqmg0
8esxIS4biPEYSkPYgMIuwGqBpd0xdJpeukS64xKi3LMM46R7HJl1kTofrXI7XwiT
0bp5EPb9Q61eu0VduPkGuIeywYXTTBDqMfzH3eC6m7M0KJRzJ7FdYgONVEf9ESvd
XrShZZB11TrJctB6anilwzHCpLGy1pHd6YlwA+pDS/dJQUtEqCaMyuLICfEfjebs
CJ1sQN8fIzIHaBwBQblT4ZkqHuUWMooxTBZ0EVLaIZVXuNllN4VWSDMmNgoNQx1L
4NsS1Ghx3cyLKGTtcUp+hT7OirZH89vOQYpWHz+t2w4HN79g1KVZRPC/ucTZFlkE
Ezy/sAfU3PWlOh9FE5YIlhejWRHdsAb+DK1Wf4HrP0qI+cWFyfNoxHcSIv3OESjh
QkxyZMW1lehVNDhlNLPknIONGkFZw7cGcVCvq2HJurPYUaHbZuz+UnPgvUicNTEB
VCkpR7ehS5MyQnOp9TW1vlNWS+BSRCAK/iWfZct1s5e1zZzglLRPgeZETvwTXTJS
EdF3srUz8of6mLwKT8j+3yRF2VtPknmIfoncKSAEKd3SZ3h2owNGheBlh75YvzlU
kA8n0ktkOaNan8+4h8jG9D/6Fn9dIpzzi3oFkzC4p5PWmeZEEiQqbJ6kt2iWqbuJ
bgthH22wNZnzjzQxAP1WDDYTNwIxR2ekmvZvIZvmvs973xsrSW6n89r7QYR4DgWX
D7A5O5TMe467keKjtadOrDWUTd3fQmKwHjpW1XhYmBbgzU6dwMbS6u37xQnweREA
PMfUztsG6mKUg5a4FFruqNAu0cnFTRTW3Fw71PuFmJtgG6ELZ+bsep5E2eDQzWL1
e6Su1fk822SGjqqzukx0fKFdVW6Mq0x7yz6Q1D7v0gPusVnnBViQW0h+onWJcdsF
UAPjBLRw627VvUc0+8PKBszz3yRn/ycIxWTryOzRtRFOPBrSsk0kBWHKh9kWWAgb
SqLAiaXPMif3vIsobJEUo6B7WE+Ktfu6JwPF7StKKPW6vFbs+Eo1RVVsuMaPkh/4
47SzMYPceq9iGta/gnjG/NaLNqbjfdvwUAjPET6khMvTlEFAk4T5KvF8EhWyuTur
fEU4s+HmBcg85x1R4k70bVK6lfZWr3RfPpsP0+v38BjFuSU9X0f7biGDQvYJTg3r
Hnf/EaZn+YmEyhoGsRt30I9/HiCiropJbczfAPQrvsrZc0y8ik7pF1kTlJT1YrSw
67N0sLnCM53ovUlQRpbkUIIdaJcKgRGAKKC/LSqfRibiZjm+tGR4iQmnVVG+0Nhi
Fd2SRaMZ0OgVXG0K8RVfrR78GtEy2SHWDfOIsF/qpk5s1ilGYC0V3SzjbB/tV0Lf
1ZsHBJ2V8wCz/KV7O8b4DehO3X1fmhLnmhnoO8o2oJH6huoJOY1f39iB19PIIG7/
Qs+HBtm1ieVCG9xdnfE5zGUtvCrPaDdwWP5fpfBs/vrJMtU23Y0xEv8=
-----END ENCRYPTED PRIVATE KEY-----
Loading
Loading