Guard --reissue against unknown CNs and leftover build files
Code review findings on the "skip revoke when .crt is missing" migration path: - CliRunner._issue() took the skip path whenever has_issued_cert() was False, which is also true for a typo'd/nonexistent CN — it would warn, skip the revoke, and go on to build, package, and email a brand-new certificate for a CN nobody asked to renew. The skip now only fires when the CN has a current index.txt entry (via _load_current_certs()); an unknown CN prints an error and exits 1 with nothing built. The TUI's _process_cert() doesn't need the same guard — renewal there always opens on an existing row (cn_readonly pins the CN), so a typo'd CN can't reach the branch. - Skipping the revoke leaves pki/reqs/<CN>.req and pki/private/<CN>.key in place (normally revoke-issued archives both), which makes EasyRSA's build-client-full abort. Both CliRunner._issue() and CursesApp._process_cert() now check for those leftovers before building and fail fast with the exact paths, rather than surfacing EasyRSA's confusing error after the CA passphrase prompt. Neither path touches the files itself. Also: strengthened two under-specified tests (test_main_rejects_bad_days_flag now checks the resolver's message text, not just "--days", which also appears in argparse's unrelated error; test_show_cert_form_confirm now pins the Enter-keypress count so a partial "days" field reversion is caught), and folded a malformed CLAUDE.md table row into its neighbor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -341,6 +341,21 @@ def has_issued_cert(pki_dir: str, cn: str) -> bool:
|
||||
return os.path.isfile(issued_cert_path(pki_dir, cn))
|
||||
|
||||
|
||||
def _leftover_build_paths(pki_dir: str, cn: str) -> List[str]:
|
||||
"""Which of pki/reqs/<CN>.req and pki/private/<CN>.key still exist.
|
||||
|
||||
`build-client-full` aborts outright if either is present — EasyRSA
|
||||
refuses to overwrite them. Normally `revoke-issued` clears both by
|
||||
archiving them into pki/revoked/, but that step is skipped when there is
|
||||
no issued cert to revoke (see has_issued_cert()), so a re-issue that
|
||||
takes the skip path must check for these leftovers itself before calling
|
||||
build-client-full, rather than let the confusing EasyRSA abort surface
|
||||
after the CA passphrase prompt.
|
||||
"""
|
||||
candidates = [f"{pki_dir}/reqs/{cn}.req", f"{pki_dir}/private/{cn}.key"]
|
||||
return [p for p in candidates if os.path.isfile(p)]
|
||||
|
||||
|
||||
def revoke_issued(
|
||||
easyrsa_dir: str, pki_dir: str, cn: str, ca_passphrase: str
|
||||
) -> None:
|
||||
@@ -1254,6 +1269,22 @@ class CursesApp:
|
||||
|
||||
revoked = False
|
||||
if is_renewal and not has_issued_cert(EASYRSA_PKI_DIR, final_cn):
|
||||
# No unknown-CN check here (unlike CliRunner._issue): final_cn is
|
||||
# cn_readonly in this form, always the CN the row was opened
|
||||
# for, which is only ever populated from _load_current_certs()
|
||||
# (see show_main_screen()/RENEW_SELECTED) — a typo'd CN can't
|
||||
# reach this branch through the TUI.
|
||||
leftovers = _leftover_build_paths(EASYRSA_PKI_DIR, final_cn)
|
||||
if leftovers:
|
||||
self._error(
|
||||
stdscr,
|
||||
f"Cannot skip revoke for {final_cn} — build-client-full "
|
||||
f"would abort:\n" + "\n".join(leftovers) + "\n\n"
|
||||
f"revoke-issued normally archives these; since there is "
|
||||
f"no issued cert to revoke, move or remove them manually "
|
||||
f"(this tool won't touch a private key), then retry.",
|
||||
)
|
||||
return True
|
||||
# index.txt lists the cert but the .crt is gone, so there is
|
||||
# nothing EasyRSA can revoke. Issue the replacement anyway.
|
||||
warning = (
|
||||
@@ -1526,6 +1557,34 @@ class CliRunner:
|
||||
|
||||
revoked = False
|
||||
if is_renewal and not has_issued_cert(EASYRSA_PKI_DIR, cn):
|
||||
# A CN with no issued .crt only takes the skip path (below) if
|
||||
# index.txt still knows it — that's the migration-gap case this
|
||||
# branch exists for. Anything else (a typo'd/nonexistent CN) must
|
||||
# not fall through to issuing a certificate nobody asked for.
|
||||
try:
|
||||
known_cns = {c.cn for c in _load_current_certs(EASYRSA_PKI_DIR)}
|
||||
except FileNotFoundError as exc:
|
||||
print(f"error: cannot read PKI index.txt: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if cn not in known_cns:
|
||||
print(
|
||||
f"error: unknown CN {cn!r} — no entry in index.txt and no "
|
||||
f"issued certificate; nothing to reissue.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
leftovers = _leftover_build_paths(EASYRSA_PKI_DIR, cn)
|
||||
if leftovers:
|
||||
print(
|
||||
f"error: cannot skip revoke for {cn} — "
|
||||
f"build-client-full would abort: " + " and ".join(leftovers) +
|
||||
" already exist. revoke-issued normally archives these; "
|
||||
"since there is no issued cert to revoke, move or remove "
|
||||
"them manually (this tool won't touch a private key), "
|
||||
"then retry.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
# index.txt lists the cert but the .crt is gone, so there is
|
||||
# nothing EasyRSA can revoke. Issue the replacement anyway.
|
||||
print(
|
||||
|
||||
Reference in New Issue
Block a user