From 2e98155b5fbd9a78477c2e6ad1844ffb1fcd91a0 Mon Sep 17 00:00:00 2001 From: Vlad Doloman Date: Sat, 15 Aug 2026 05:16:41 +0300 Subject: [PATCH] Pass --days to build-client-full when a lifetime is set Co-Authored-By: Claude Opus 5 --- openvpncertupdate.py | 8 ++++++-- tests/test_easyrsa.py | 47 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/openvpncertupdate.py b/openvpncertupdate.py index b3316ee..6ba541b 100644 --- a/openvpncertupdate.py +++ b/openvpncertupdate.py @@ -352,12 +352,16 @@ def revoke_issued( def build_client_full( easyrsa_dir: str, pki_dir: str, cn: str, - key_passphrase: str, ca_passphrase: str, email: str = "", + key_passphrase: str, ca_passphrase: str, email: str = "", days: str = "", ) -> None: extra_env = {"EASYRSA_REQ_EMAIL": email} if email else None _run_easyrsa( _base_cmd(easyrsa_dir, pki_dir, ca_passphrase) - + [f"--passout=pass:{key_passphrase}", "build-client-full", cn], + + [f"--passout=pass:{key_passphrase}"] + # Global option, so it must precede the verb. Deliberately not in + # _base_cmd(): gen-crl reads --days as CRL validity instead. + + ([f"--days={days}"] if days else []) + + ["build-client-full", cn], cwd=easyrsa_dir, extra_env=extra_env, ) diff --git a/tests/test_easyrsa.py b/tests/test_easyrsa.py index f72a0d8..61c5754 100644 --- a/tests/test_easyrsa.py +++ b/tests/test_easyrsa.py @@ -286,3 +286,50 @@ def test_resolve_cert_days_error_names_the_source(): # must say which one the user actually touched. with pytest.raises(ConfigError, match="--days"): resolve_cert_days("0", "--days") + + +# --------------------------------------------------------------------------- +# build_client_full with days parameter +# --------------------------------------------------------------------------- + +@patch("openvpncertupdate.subprocess.run") +def test_build_client_full_includes_days_when_set(mock_run): + mock_run.return_value = ok_result() + build_client_full("/er", "/pki", "bob", "keypass", "capass", days="90") + assert "--days=90" in mock_run.call_args[0][0] + + +@patch("openvpncertupdate.subprocess.run") +def test_build_client_full_omits_days_when_inheriting(mock_run): + mock_run.return_value = ok_result() + build_client_full("/er", "/pki", "bob", "keypass", "capass", days="") + assert not any(a.startswith("--days") for a in mock_run.call_args[0][0]) + + +@patch("openvpncertupdate.subprocess.run") +def test_build_client_full_days_precedes_the_verb(mock_run): + # --days is a global option: EasyRSA parses it before the command word. + mock_run.return_value = ok_result() + build_client_full("/er", "/pki", "bob", "keypass", "capass", days="90") + args = mock_run.call_args[0][0] + assert args.index("--days=90") < args.index("build-client-full") + + +@patch("openvpncertupdate.subprocess.run") +def test_revoke_and_gen_crl_never_carry_days(mock_run): + # _base_cmd() is shared with gen-crl, where --days means CRL validity. + # Putting the flag there would quietly change how long CRLs are valid. + mock_run.return_value = ok_result() + revoke_issued("/er", "/pki", "alice", "capass") + assert not any(a.startswith("--days") for a in mock_run.call_args[0][0]) + gen_crl("/er", "/pki", "capass") + assert not any(a.startswith("--days") for a in mock_run.call_args[0][0]) + + +@patch("openvpncertupdate.subprocess.run") +def test_error_verb_detection_survives_days_flag(mock_run): + # _run_easyrsa picks the verb as the first non-flag arg; "--days=90" + # must not be mistaken for it. + mock_run.return_value = err_result(stdout="Error\nboom", stderr="") + with pytest.raises(EasyRSAError, match="build-client-full"): + build_client_full("/er", "/pki", "bob", "keypass", "capass", days="90")