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:
@@ -65,6 +65,7 @@ class CertInfo:
|
|||||||
cn: str
|
cn: str
|
||||||
expires: datetime # UTC
|
expires: datetime # UTC
|
||||||
days_left: int # negative = already expired
|
days_left: int # negative = already expired
|
||||||
|
email: str = ""
|
||||||
|
|
||||||
|
|
||||||
def _parse_date(raw: str) -> datetime:
|
def _parse_date(raw: str) -> datetime:
|
||||||
@@ -80,8 +81,8 @@ def _parse_date(raw: str) -> datetime:
|
|||||||
return dt.replace(tzinfo=timezone.utc)
|
return dt.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
def _parse_index_line(line: str) -> Optional[tuple[str, datetime]]:
|
def _parse_index_line(line: str) -> Optional[tuple[str, datetime, str]]:
|
||||||
"""Return (cn, expiry) for valid (V-status) index.txt lines, else None."""
|
"""Return (cn, expiry, email) for valid (V-status) index.txt lines, else None."""
|
||||||
parts = line.rstrip("\n").split("\t")
|
parts = line.rstrip("\n").split("\t")
|
||||||
if len(parts) < 6 or parts[0] != "V":
|
if len(parts) < 6 or parts[0] != "V":
|
||||||
return None
|
return None
|
||||||
@@ -91,13 +92,15 @@ def _parse_index_line(line: str) -> Optional[tuple[str, datetime]]:
|
|||||||
return None
|
return None
|
||||||
dn = parts[5]
|
dn = parts[5]
|
||||||
cn = None
|
cn = None
|
||||||
|
email = ""
|
||||||
for seg in dn.split("/"):
|
for seg in dn.split("/"):
|
||||||
if seg.startswith("CN="):
|
if seg.startswith("CN="):
|
||||||
cn = seg[3:]
|
cn = seg[3:]
|
||||||
break
|
elif seg.startswith("emailAddress="):
|
||||||
|
email = seg[len("emailAddress="):]
|
||||||
if not cn:
|
if not cn:
|
||||||
return None
|
return None
|
||||||
return cn, expiry
|
return cn, expiry, email
|
||||||
|
|
||||||
|
|
||||||
def load_expiring_certs(
|
def load_expiring_certs(
|
||||||
@@ -115,12 +118,13 @@ def load_expiring_certs(
|
|||||||
parsed = _parse_index_line(line)
|
parsed = _parse_index_line(line)
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
continue
|
continue
|
||||||
cn, expiry = parsed
|
cn, expiry, email = parsed
|
||||||
if cutoff_past <= expiry <= cutoff_future:
|
if cutoff_past <= expiry <= cutoff_future:
|
||||||
results.append(CertInfo(
|
results.append(CertInfo(
|
||||||
cn=cn,
|
cn=cn,
|
||||||
expires=expiry,
|
expires=expiry,
|
||||||
days_left=(expiry - now).days,
|
days_left=(expiry - now).days,
|
||||||
|
email=email,
|
||||||
))
|
))
|
||||||
results.sort(key=lambda c: c.expires)
|
results.sort(key=lambda c: c.expires)
|
||||||
return results
|
return results
|
||||||
@@ -135,11 +139,12 @@ def load_all_certs(pki_dir: str) -> list[CertInfo]:
|
|||||||
parsed = _parse_index_line(line)
|
parsed = _parse_index_line(line)
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
continue
|
continue
|
||||||
cn, expiry = parsed
|
cn, expiry, email = parsed
|
||||||
results.append(CertInfo(
|
results.append(CertInfo(
|
||||||
cn=cn,
|
cn=cn,
|
||||||
expires=expiry,
|
expires=expiry,
|
||||||
days_left=(expiry - now).days,
|
days_left=(expiry - now).days,
|
||||||
|
email=email,
|
||||||
))
|
))
|
||||||
results.sort(key=lambda c: c.expires)
|
results.sort(key=lambda c: c.expires)
|
||||||
return results
|
return results
|
||||||
@@ -850,8 +855,11 @@ class CursesApp:
|
|||||||
elif result.action == Action.NEW_CERT:
|
elif result.action == Action.NEW_CERT:
|
||||||
self._process_cert(stdscr, cn="", email="", is_renewal=False)
|
self._process_cert(stdscr, cn="", email="", is_renewal=False)
|
||||||
elif result.action == Action.RENEW_SELECTED:
|
elif result.action == Action.RENEW_SELECTED:
|
||||||
|
cert_by_cn = {c.cn: c for c in certs}
|
||||||
for cn in result.selected_cns:
|
for cn in result.selected_cns:
|
||||||
email_addr = get_email(EASYRSA_PKI_DIR, cn)
|
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,
|
if not self._process_cert(stdscr, cn=cn, email=email_addr,
|
||||||
is_renewal=True):
|
is_renewal=True):
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -54,9 +54,40 @@ def test_parse_4digit_year():
|
|||||||
line = "V\t20251231120000Z\t\t07\tunknown\t/CN=future"
|
line = "V\t20251231120000Z\t\t07\tunknown\t/CN=future"
|
||||||
result = _parse_index_line(line)
|
result = _parse_index_line(line)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
cn, dt = result
|
cn, dt, email = result
|
||||||
assert cn == "future"
|
assert cn == "future"
|
||||||
assert dt.year == 2025
|
assert dt.year == 2025
|
||||||
|
assert email == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_email_from_dn():
|
||||||
|
line = "V\t20251231120000Z\t\t07\tunknown\t/CN=alice/emailAddress=alice@example.com"
|
||||||
|
result = _parse_index_line(line)
|
||||||
|
assert result is not None
|
||||||
|
cn, dt, email = result
|
||||||
|
assert cn == "alice"
|
||||||
|
assert email == "alice@example.com"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_email_absent_returns_empty():
|
||||||
|
line = "V\t20251231120000Z\t\t07\tunknown\t/CN=bob"
|
||||||
|
result = _parse_index_line(line)
|
||||||
|
assert result is not None
|
||||||
|
cn, dt, email = result
|
||||||
|
assert cn == "bob"
|
||||||
|
assert email == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_expiring_certs_populates_email(tmp_path):
|
||||||
|
now = datetime.now(tz=timezone.utc)
|
||||||
|
pki = tmp_path / "pki"
|
||||||
|
pki.mkdir()
|
||||||
|
expiry = now + timedelta(days=5)
|
||||||
|
line = f"V\t{expiry.strftime('%y%m%d%H%M%S')}Z\t\t01\tunknown\t/CN=carol/emailAddress=carol@example.com\n"
|
||||||
|
(pki / "index.txt").write_text(line)
|
||||||
|
certs = load_expiring_certs(str(pki), days_past=0, days_ahead=14)
|
||||||
|
assert len(certs) == 1
|
||||||
|
assert certs[0].email == "carol@example.com"
|
||||||
|
|
||||||
|
|
||||||
def test_load_all_certs_includes_all_valid(tmp_path):
|
def test_load_all_certs_includes_all_valid(tmp_path):
|
||||||
|
|||||||
Reference in New Issue
Block a user