From 869f3d2338fed05bc11baac6ffa25301aa9be83d Mon Sep 17 00:00:00 2001 From: Dmytro Hissa Date: Wed, 8 Jul 2026 12:57:52 +0200 Subject: [PATCH 1/2] Add CA bundle version and pinning status to the user agent string --- duo_client/client.py | 10 +++++++++- tests/test_client.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/duo_client/client.py b/duo_client/client.py index 1748aee..356d934 100644 --- a/duo_client/client.py +++ b/duo_client/client.py @@ -36,6 +36,7 @@ from .https_wrapper import CertValidatingHTTPSConnection DEFAULT_CA_CERTS = os.path.join(os.path.dirname(__file__), 'ca_certs.pem') +CA_BUNDLE_VERSION = '1.0' def canon_params(params): @@ -243,7 +244,14 @@ def __init__(self, ikey, skey, host, if ca_certs is None: ca_certs = DEFAULT_CA_CERTS self.ca_certs = ca_certs - self.user_agent = user_agent + ca_pinning_status = 'disabled' if disable_ca_pinning else 'enabled' + if user_agent: + self.user_agent = ( + f"{user_agent} ca_bundle/{CA_BUNDLE_VERSION}" + f" ca_pinning/{ca_pinning_status}" + ) + else: + self.user_agent = user_agent self.set_proxy(host=None, proxy_type=None) self.paging_limit = paging_limit self.digestmod = digestmod diff --git a/tests/test_client.py b/tests/test_client.py index 6128f27..3183359 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -806,6 +806,50 @@ def test_disable_ca_pinning_with_disable_ca_certs_raises(self): self.assertIn("Cannot both disable CA pinning", str(ctx.exception)) +class TestUserAgent(unittest.TestCase): + """Tests for user agent string including CA bundle version and pinning status.""" + + def test_default_user_agent_includes_ca_bundle_version(self): + client = duo_client.client.Client('ikey', 'skey', 'host.example.com') + self.assertIn( + f"ca_bundle/{duo_client.client.CA_BUNDLE_VERSION}", + client.user_agent, + ) + + def test_default_user_agent_includes_ca_pinning_enabled(self): + client = duo_client.client.Client('ikey', 'skey', 'host.example.com') + self.assertIn("ca_pinning/enabled", client.user_agent) + + def test_user_agent_includes_ca_pinning_disabled(self): + client = duo_client.client.Client('ikey', 'skey', 'host.example.com', + disable_ca_pinning=True) + self.assertIn("ca_pinning/disabled", client.user_agent) + + def test_default_user_agent_format(self): + client = duo_client.client.Client('ikey', 'skey', 'host.example.com') + expected = ( + f"Duo API Python/{duo_client.client.__version__}" + f" ca_bundle/{duo_client.client.CA_BUNDLE_VERSION}" + f" ca_pinning/enabled" + ) + self.assertEqual(client.user_agent, expected) + + def test_custom_user_agent_includes_ca_fields(self): + client = duo_client.client.Client('ikey', 'skey', 'host.example.com', + user_agent='MyApp/1.0') + expected = ( + f"MyApp/1.0" + f" ca_bundle/{duo_client.client.CA_BUNDLE_VERSION}" + f" ca_pinning/enabled" + ) + self.assertEqual(client.user_agent, expected) + + def test_empty_user_agent_not_modified(self): + client = duo_client.client.Client('ikey', 'skey', 'host.example.com', + user_agent='') + self.assertEqual(client.user_agent, '') + + class TestDisableCaPinningConnect(unittest.TestCase): """Tests that _connect() uses the correct connection type.""" From cd012714a84847c78fda2af20574d8887ee8faf3 Mon Sep 17 00:00:00 2001 From: Dmytro Hissa Date: Thu, 9 Jul 2026 11:14:58 +0200 Subject: [PATCH 2/2] Updated the user agent format --- duo_client/client.py | 2 +- tests/test_client.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/duo_client/client.py b/duo_client/client.py index 356d934..cc87d3c 100644 --- a/duo_client/client.py +++ b/duo_client/client.py @@ -248,7 +248,7 @@ def __init__(self, ikey, skey, host, if user_agent: self.user_agent = ( f"{user_agent} ca_bundle/{CA_BUNDLE_VERSION}" - f" ca_pinning/{ca_pinning_status}" + f" (ca_pinning={ca_pinning_status})" ) else: self.user_agent = user_agent diff --git a/tests/test_client.py b/tests/test_client.py index 3183359..ce7f300 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -818,19 +818,19 @@ def test_default_user_agent_includes_ca_bundle_version(self): def test_default_user_agent_includes_ca_pinning_enabled(self): client = duo_client.client.Client('ikey', 'skey', 'host.example.com') - self.assertIn("ca_pinning/enabled", client.user_agent) + self.assertIn("(ca_pinning=enabled)", client.user_agent) def test_user_agent_includes_ca_pinning_disabled(self): client = duo_client.client.Client('ikey', 'skey', 'host.example.com', disable_ca_pinning=True) - self.assertIn("ca_pinning/disabled", client.user_agent) + self.assertIn("(ca_pinning=disabled)", client.user_agent) def test_default_user_agent_format(self): client = duo_client.client.Client('ikey', 'skey', 'host.example.com') expected = ( f"Duo API Python/{duo_client.client.__version__}" f" ca_bundle/{duo_client.client.CA_BUNDLE_VERSION}" - f" ca_pinning/enabled" + f" (ca_pinning=enabled)" ) self.assertEqual(client.user_agent, expected) @@ -840,7 +840,7 @@ def test_custom_user_agent_includes_ca_fields(self): expected = ( f"MyApp/1.0" f" ca_bundle/{duo_client.client.CA_BUNDLE_VERSION}" - f" ca_pinning/enabled" + f" (ca_pinning=enabled)" ) self.assertEqual(client.user_agent, expected)