From b9c608fcbb36b65566228ced0c6b02c2b71e35a5 Mon Sep 17 00:00:00 2001 From: "matthew.gill" Date: Wed, 11 Feb 2026 12:38:04 -0800 Subject: [PATCH 1/3] fix: Properly support mTLS --- docs/user/authentication.rst | 32 +++++++++++++++++++++++++++++++- pysnc/client.py | 3 ++- test/test_snc_auth.py | 13 +++++++++++++ test/test_snc_element.py | 3 +++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/docs/user/authentication.rst b/docs/user/authentication.rst index 8f5c019..78683eb 100644 --- a/docs/user/authentication.rst +++ b/docs/user/authentication.rst @@ -68,7 +68,37 @@ mTLS - Mutual TLS Authentication (Certificate-Based Authentication) The most ideal form of authentication for machine to machine communication. Follow `KB0993615 `_ then: - >>> client = ServiceNowClient(instance, cert=('/path/to/client.cert', '/path/to/client.key')) + >>> client = ServiceNowClient(instance, cert=('/path/to/USER_x509.pem', '/path/to/USERPRIVATEKEY.key')) + + +A quick example, using self-signed certificates: + +1. Setup the CA (root) key + +```bash +# generate a root private key, if for some reason you don't have one already +$ openssl genrsa -aes256 -out ca.key 2048 +# generate the CA certificate +$ openssl req -x509 -new -nodes -key ca.key -out cert.pem -sha512 -days 365 -out cacert.pem +``` + +2. Upload cacert.pem via /sys_ca_certificate.do + +3. Setup the user key and CSR (we just generate them here for a POC example) + +```bash +# note: python requests (the underlying library) does not directly support keys with passwords! +$ openssl req -nodes -newkey rsa:2048 -keyout USERPRIVATEKEY.key -out USERCSR.csr +``` + +4. Sign the CSR with the root, creating a X.509 for the user + +``` +$ openssl x509 -req -days 365 -in USERCSR.csr -CA cacert.pem -CAkey ca.key -extfile <(printf "extendedKeyUsage=clientAuth") -out USER_x509.pem +``` + +5. Attach `USER_x509.pem` to a new `/sys_user_certificate.do` record + Requests Authentication ----------------------- diff --git a/pysnc/client.py b/pysnc/client.py index 218dc21..a1df47b 100644 --- a/pysnc/client.py +++ b/pysnc/client.py @@ -29,7 +29,7 @@ class ServiceNowClient(object): :param bool verify: Verify the SSL/TLS certificate OR the certificate to use. Useful if you're using a self-signed HTTPS proxy. :param cert: if String, path to ssl client cert file (.pem). If Tuple, (‘cert’, ‘key’) pair. """ - def __init__(self, instance, auth, proxy=None, verify=None, cert=None, auto_retry=True): + def __init__(self, instance, auth=None, proxy=None, verify=None, cert=None, auto_retry=True): self._log = logging.getLogger(__name__) self.__instance = get_instance(instance) @@ -62,6 +62,7 @@ def __init__(self, instance, auth, proxy=None, verify=None, cert=None, auto_retr elif isinstance(auth, ServiceNowFlow): self.__session = auth.authenticate(self.__instance, proxies=self.__proxies, verify=verify) elif cert is not None: + self.__session = requests.session() self.__session.cert = cert else: raise AuthenticationException('No valid authentication method provided') diff --git a/test/test_snc_auth.py b/test/test_snc_auth.py index d1d2415..0fc0732 100644 --- a/test/test_snc_auth.py +++ b/test/test_snc_auth.py @@ -64,6 +64,19 @@ def nop_test_jwt(self): assert gr.get('6816f79cc0a8016401c5a33be04be441'), "did not jwt auth" ''' + @skip("Requires keys and conf that makes automation hard") + def test_mtls(self): + # e.g. PYSNC_USER_KEY=x PYSNC_USER_CERT=y poetry run pytest test/test_snc_auth.py::TestAuth::test_mtls + path_key = self.c.get_value('USER_KEY') + assert path_key, 'Require user private key' + path_cert = self.c.get_value('USER_CERT') + assert path_cert, 'Require user x509 certificate' + + client = ServiceNowClient(self.c.server, cert=(path_cert, path_key)) + gr = client.GlideRecord('sys_user') + gr.fields = 'sys_id' + self.assertTrue(gr.get('6816f79cc0a8016401c5a33be04be441')) + diff --git a/test/test_snc_element.py b/test/test_snc_element.py index b9e1c7d..915dce1 100644 --- a/test/test_snc_element.py +++ b/test/test_snc_element.py @@ -217,3 +217,6 @@ def test_changes(self): self.assertFalse(element.changes()) element.set_value('4') self.assertTrue(element.changes()) + + def test_non_ref_string_field(self): + element = GlideElement('') \ No newline at end of file From 38f03148ba76f5812df8efa38324377e8c90f33f Mon Sep 17 00:00:00 2001 From: "matthew.gill" Date: Wed, 11 Feb 2026 13:00:15 -0800 Subject: [PATCH 2/3] docs: update mTLS update docs --- docs/user/authentication.rst | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/user/authentication.rst b/docs/user/authentication.rst index 78683eb..ef3a03f 100644 --- a/docs/user/authentication.rst +++ b/docs/user/authentication.rst @@ -68,6 +68,7 @@ mTLS - Mutual TLS Authentication (Certificate-Based Authentication) The most ideal form of authentication for machine to machine communication. Follow `KB0993615 `_ then: + >>> client = ServiceNowClient(instance, cert=('/path/to/USER_x509.pem', '/path/to/USERPRIVATEKEY.key')) @@ -75,27 +76,30 @@ A quick example, using self-signed certificates: 1. Setup the CA (root) key -```bash -# generate a root private key, if for some reason you don't have one already -$ openssl genrsa -aes256 -out ca.key 2048 -# generate the CA certificate -$ openssl req -x509 -new -nodes -key ca.key -out cert.pem -sha512 -days 365 -out cacert.pem -``` +.. code-block:: bash + + # generate a root private key, if for some reason you don't have one already + openssl genrsa -aes256 -out ca.key 2048 + # generate the CA certificate + openssl req -x509 -new -nodes -key ca.key -out cert.pem -sha512 -days 365 -out cacert.pem -2. Upload cacert.pem via /sys_ca_certificate.do +2. Upload `cacert.pem` via `/sys_ca_certificate.do` 3. Setup the user key and CSR (we just generate them here for a POC example) -```bash -# note: python requests (the underlying library) does not directly support keys with passwords! -$ openssl req -nodes -newkey rsa:2048 -keyout USERPRIVATEKEY.key -out USERCSR.csr -``` +.. code-block:: bash + + openssl req -nodes -newkey rsa:2048 -keyout USERPRIVATEKEY.key -out USERCSR.csr + +.. important:: + + Python requests (the underlying http library) does not directly support keys with passwords! See `requests#2519 `_ for details. 4. Sign the CSR with the root, creating a X.509 for the user -``` -$ openssl x509 -req -days 365 -in USERCSR.csr -CA cacert.pem -CAkey ca.key -extfile <(printf "extendedKeyUsage=clientAuth") -out USER_x509.pem -``` +.. code-block:: bash + + openssl x509 -req -days 365 -in USERCSR.csr -CA cacert.pem -CAkey ca.key -extfile <(printf "extendedKeyUsage=clientAuth") -out USER_x509.pem 5. Attach `USER_x509.pem` to a new `/sys_user_certificate.do` record From 94be796bd4ee5b30d45135a0218d2d5f32899b7e Mon Sep 17 00:00:00 2001 From: "matthew.gill" Date: Wed, 11 Feb 2026 13:01:44 -0800 Subject: [PATCH 3/3] fix!: revert accidental change --- test/test_snc_element.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/test_snc_element.py b/test/test_snc_element.py index 915dce1..b9e1c7d 100644 --- a/test/test_snc_element.py +++ b/test/test_snc_element.py @@ -217,6 +217,3 @@ def test_changes(self): self.assertFalse(element.changes()) element.set_value('4') self.assertTrue(element.changes()) - - def test_non_ref_string_field(self): - element = GlideElement('') \ No newline at end of file