From 5e8b7b2a9e899e8aefff5d7a3ee27f9cd63dc484 Mon Sep 17 00:00:00 2001 From: Aaron McConnell Date: Thu, 27 Aug 2026 12:03:34 -0400 Subject: [PATCH] fix: pad HMAC key to 64 bytes to avoid InsecureKeyLengthWarning (#37) Duo client secrets are 40 bytes, below the 64-byte minimum PyJWT recommends for HS512 (RFC 7518 Section 3.2), so PyJWT emits an InsecureKeyLengthWarning on every jwt.encode call. Zero-pad the secret to 64 bytes before using it as the HMAC key. HMAC zero-pads the key to the hash block size (128 bytes for SHA-512) regardless, so this produces a byte-identical signature and remains wire-compatible with Duo while silencing the warning. This mirrors the fix in the duo_universal_csharp library (PR #23). Co-Authored-By: Claude Opus 4.8 --- duo_universal/client.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/duo_universal/client.py b/duo_universal/client.py index c6241d7..a44ef33 100644 --- a/duo_universal/client.py +++ b/duo_universal/client.py @@ -11,6 +11,12 @@ CLIENT_ID_LENGTH = 20 CLIENT_SECRET_LENGTH = 40 +# Minimum recommended HMAC key length in bytes for SHA512 (RFC 7518 Section 3.2). +# Duo client secrets are shorter than this, so the key is zero-padded up to this +# length before signing/verifying. HMAC zero-pads the key to the hash block size +# regardless, so padding produces an identical signature but avoids PyJWT's +# InsecureKeyLengthWarning. +MINIMUM_HMAC_KEY_LENGTH = 64 JTI_LENGTH = 36 MINIMUM_STATE_LENGTH = 16 MAXIMUM_STATE_LENGTH = 1024 @@ -159,6 +165,10 @@ def __init__(self, client_id, client_secret, host, self._client_id = client_id self._client_secret = client_secret + # Zero-pad the secret up to the minimum recommended HMAC key length so + # PyJWT does not emit an InsecureKeyLengthWarning. This does not change + # the resulting signature (see MINIMUM_HMAC_KEY_LENGTH). + self._signing_key = client_secret.encode('utf-8').ljust(MINIMUM_HMAC_KEY_LENGTH, b'\x00') self._api_host = host self._redirect_uri = redirect_uri self._use_duo_code_attribute = use_duo_code_attribute @@ -212,7 +222,7 @@ def health_check(self): all_args = { 'client_assertion': jwt.encode(jwt_args, - self._client_secret, + self._signing_key, algorithm='HS512'), 'client_id': self._client_id } @@ -264,7 +274,7 @@ def create_auth_url(self, username, state, nonce=None): } request_jwt = jwt.encode(jwt_args, - self._client_secret, + self._signing_key, algorithm='HS512') all_args = { 'response_type': 'code', @@ -313,7 +323,7 @@ def exchange_authorization_code_for_2fa_result(self, duoCode, username, nonce=No 'client_id': self._client_id, 'client_assertion_type': CLIENT_ASSERT_TYPE, 'client_assertion': jwt.encode(jwt_args, - self._client_secret, + self._signing_key, algorithm='HS512') } try: @@ -342,7 +352,7 @@ def exchange_authorization_code_for_2fa_result(self, duoCode, username, nonce=No try: decoded_token = jwt.decode( response.json()['id_token'], - self._client_secret, + self._signing_key, audience=self._client_id, issuer=OAUTH_V1_TOKEN_ENDPOINT.format(self._api_host), leeway=LEEWAY,