fix: populate cert form email from index.txt emailAddress field on reissue

_parse_index_line now extracts the emailAddress segment from the DN
alongside CN, returning a (cn, expiry, email) triple. CertInfo gains an
email field populated by both load functions.

In _main(), RENEW_SELECTED falls back to cert.email when the metadata
store has no entry for the CN — so certs created outside this tool (or
before the metadata store existed) pre-fill the email field correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vlad Doloman
2026-06-24 12:33:25 +03:00
parent eb47e1262f
commit f0db11597a
2 changed files with 46 additions and 7 deletions

View File

@@ -65,6 +65,7 @@ class CertInfo:
cn: str
expires: datetime # UTC
days_left: int # negative = already expired
email: str = ""
def _parse_date(raw: str) -> datetime:
@@ -80,8 +81,8 @@ def _parse_date(raw: str) -> datetime:
return dt.replace(tzinfo=timezone.utc)
def _parse_index_line(line: str) -> Optional[tuple[str, datetime]]:
"""Return (cn, expiry) for valid (V-status) index.txt lines, else None."""
def _parse_index_line(line: str) -> Optional[tuple[str, datetime, str]]:
"""Return (cn, expiry, email) for valid (V-status) index.txt lines, else None."""
parts = line.rstrip("\n").split("\t")
if len(parts) < 6 or parts[0] != "V":
return None
@@ -91,13 +92,15 @@ def _parse_index_line(line: str) -> Optional[tuple[str, datetime]]:
return None
dn = parts[5]
cn = None
email = ""
for seg in dn.split("/"):
if seg.startswith("CN="):
cn = seg[3:]
break
elif seg.startswith("emailAddress="):
email = seg[len("emailAddress="):]
if not cn:
return None
return cn, expiry
return cn, expiry, email
def load_expiring_certs(
@@ -115,12 +118,13 @@ def load_expiring_certs(
parsed = _parse_index_line(line)
if parsed is None:
continue
cn, expiry = parsed
cn, expiry, email = parsed
if cutoff_past <= expiry <= cutoff_future:
results.append(CertInfo(
cn=cn,
expires=expiry,
days_left=(expiry - now).days,
email=email,
))
results.sort(key=lambda c: c.expires)
return results
@@ -135,11 +139,12 @@ def load_all_certs(pki_dir: str) -> list[CertInfo]:
parsed = _parse_index_line(line)
if parsed is None:
continue
cn, expiry = parsed
cn, expiry, email = parsed
results.append(CertInfo(
cn=cn,
expires=expiry,
days_left=(expiry - now).days,
email=email,
))
results.sort(key=lambda c: c.expires)
return results
@@ -850,8 +855,11 @@ class CursesApp:
elif result.action == Action.NEW_CERT:
self._process_cert(stdscr, cn="", email="", is_renewal=False)
elif result.action == Action.RENEW_SELECTED:
cert_by_cn = {c.cn: c for c in certs}
for cn in result.selected_cns:
email_addr = get_email(EASYRSA_PKI_DIR, cn)
if not email_addr and cn in cert_by_cn:
email_addr = cert_by_cn[cn].email
if not self._process_cert(stdscr, cn=cn, email=email_addr,
is_renewal=True):
break