dedupe cert list rows to the last non-revoked entry per CN
index.txt is append-only: a CN left unrevoked after expiring, then reissued, accumulates multiple V-status lines. load_expiring_certs() and load_all_certs() previously returned one CertInfo per line, so such a CN showed as duplicate rows in the TUI main menu (and in --list/--list-all). Extract the existing "last non-revoked entry wins" scan (previously only in get_email()) into a shared _load_current_certs(), and build load_expiring_certs()/load_all_certs()/get_email() on top of it. The date-window filter in load_expiring_certs() now applies to each CN's canonical (most recent) entry rather than to raw lines, so a stale duplicate that happens to fall in the "recently expired" window no longer masks a live reissued cert whose real expiry is outside it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -165,71 +165,61 @@ def _parse_index_line(line: str) -> Optional[tuple[str, datetime, str]]:
|
||||
return cn, expiry, email
|
||||
|
||||
|
||||
def _load_current_certs(pki_dir: str) -> list[CertInfo]:
|
||||
"""Return one CertInfo per CN: its most recent V-status index.txt entry.
|
||||
|
||||
index.txt is append-only, so a CN can accumulate several V-status lines
|
||||
(e.g. an old cert left unrevoked after it expired, then reissued) as
|
||||
well as older R-status (revoked) ones. Scan the whole file in order and
|
||||
keep only the last non-revoked line per CN — same rule get_email() uses
|
||||
for picking an email among duplicates, applied here to the row itself
|
||||
so expiring/all-certs views don't show stale duplicate rows.
|
||||
"""
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
by_cn: dict[str, CertInfo] = {}
|
||||
with open(os.path.join(pki_dir, "index.txt")) as fh:
|
||||
for line in fh:
|
||||
parsed = _parse_index_line(line)
|
||||
if parsed is None:
|
||||
continue
|
||||
cn, expiry, email = parsed
|
||||
by_cn[cn] = CertInfo(
|
||||
cn=cn,
|
||||
expires=expiry,
|
||||
days_left=(expiry - now).days,
|
||||
email=email,
|
||||
)
|
||||
return list(by_cn.values())
|
||||
|
||||
|
||||
def load_expiring_certs(
|
||||
pki_dir: str,
|
||||
days_past: int,
|
||||
days_ahead: int,
|
||||
) -> list[CertInfo]:
|
||||
"""Return valid certs whose expiry falls within [-days_past, +days_ahead]."""
|
||||
"""Return current certs whose expiry falls within [-days_past, +days_ahead]."""
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
cutoff_past = now - timedelta(days=days_past)
|
||||
cutoff_future = now + timedelta(days=days_ahead)
|
||||
results: list[CertInfo] = []
|
||||
with open(os.path.join(pki_dir, "index.txt")) as fh:
|
||||
for line in fh:
|
||||
parsed = _parse_index_line(line)
|
||||
if parsed is None:
|
||||
continue
|
||||
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 = [c for c in _load_current_certs(pki_dir)
|
||||
if cutoff_past <= c.expires <= cutoff_future]
|
||||
results.sort(key=lambda c: c.expires)
|
||||
return results
|
||||
|
||||
|
||||
def load_all_certs(pki_dir: str) -> list[CertInfo]:
|
||||
"""Return all valid (V-status) certs, sorted by expiry date."""
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
results: list[CertInfo] = []
|
||||
with open(os.path.join(pki_dir, "index.txt")) as fh:
|
||||
for line in fh:
|
||||
parsed = _parse_index_line(line)
|
||||
if parsed is None:
|
||||
continue
|
||||
cn, expiry, email = parsed
|
||||
results.append(CertInfo(
|
||||
cn=cn,
|
||||
expires=expiry,
|
||||
days_left=(expiry - now).days,
|
||||
email=email,
|
||||
))
|
||||
"""Return all current certs, sorted by expiry date."""
|
||||
results = _load_current_certs(pki_dir)
|
||||
results.sort(key=lambda c: c.expires)
|
||||
return results
|
||||
|
||||
|
||||
def get_email(pki_dir: str, cn: str) -> str:
|
||||
"""Return the emailAddress from CN's most recent V-status index.txt entry.
|
||||
|
||||
index.txt is append-only, so a CN can have several V-status lines (e.g.
|
||||
issued directly with easyrsa, bypassing this tool's revoke-before-build
|
||||
renewal flow) as well as older R-status (revoked) ones; scan the whole
|
||||
file in order and keep the last non-revoked match, or '' if none.
|
||||
"""
|
||||
found = ""
|
||||
with open(os.path.join(pki_dir, "index.txt")) as fh:
|
||||
for line in fh:
|
||||
parsed = _parse_index_line(line)
|
||||
if parsed is None:
|
||||
continue
|
||||
line_cn, _expiry, email = parsed
|
||||
if line_cn == cn:
|
||||
found = email
|
||||
return found
|
||||
"""Return the emailAddress on CN's current (most recent V-status) entry."""
|
||||
for c in _load_current_certs(pki_dir):
|
||||
if c.cn == cn:
|
||||
return c.email
|
||||
return ""
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
||||
Reference in New Issue
Block a user