32 Commits

Author SHA1 Message Date
Vlad Doloman
b8158e260a Restore Python 3.6 compatibility
Remove Python 3.7+ constructs so the tool runs on 3.6:

- Drop `from __future__ import annotations` (3.7+) and convert all
  builtin-generic annotations (list[]/dict[]/tuple[]/set[]) to their
  typing.List/Dict/Tuple/Set equivalents, since without the future
  import these annotations are evaluated eagerly and builtin generic
  subscription only exists on 3.9+.
- Replace subprocess.run(capture_output=, text=) — both 3.7+ — with
  stdout/stderr=PIPE and universal_newlines=True; update the matching
  test assertion.
- requirements.txt: gate cryptography>=41 behind python_version>=3.7 and
  pin cryptography<41 (last 3.6-compatible line, 40.0.2) plus the
  dataclasses backport for 3.6.
- Suppress cryptography 40.x's per-import "Python 3.6 is no longer
  supported" deprecation warning so it doesn't pollute CLI/cron stderr;
  the <41 pin keeps us on a supported release regardless.

The only cryptography API used is AESGCM (present since 2.0), so no
x509/API-surface changes are needed for the older pin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:12:41 +03:00
Vlad Doloman
0477392b5e regenerate and copy CRL during reissue; run restorecon after every CRL copy
Reissue internally revokes the old cert (revoke_issued), which makes
the published CRL immediately stale, but neither the CLI --reissue
path nor the TUI renew flow ever regenerated/copied it — only
standalone revoke and "Regenerate CRL" did. A cert revoked as part of
a reissue could keep authenticating against OpenVPN until someone
remembered to regenerate the CRL by hand.

Both reissue paths now call gen_crl()+copy_crl() right after a
successful revoke_issued(), same as standalone revoke. This isn't
gated on the subsequent build_client_full() succeeding, since the CRL
is already stale the moment the old cert is revoked. A CRL-regen
failure here is reported as a warning and the reissue continues to
build the replacement cert — a new cert is more urgent than a fresh
CRL, and "Regenerate CRL"/--gen-crl remain available to retry.

Also add a RESTORECON_BINARY setting (default "restorecon", "" to
disable): copy_crl() now runs it on the destination file after every
copy, everywhere copy_crl() is called. Best-effort like
is_ca_key_encrypted() — a missing/misconfigured binary is swallowed
rather than breaking CRL deployment on non-SELinux systems.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 10:35:17 +03:00
Vlad Doloman
bb11702c4f 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>
2026-07-08 17:52:26 +03:00
Vlad Doloman
2640dbf81d use erase() instead of clear() in per-keystroke TUI redraw loops
clear() forces a full physical screen repaint on the next refresh();
erase() just blanks the buffer content and lets curses diff against
what's already on the terminal, sending only the changed cells. The
main cert list and the cert-entry form both redraw on every keystroke,
so clear() there meant retransmitting the whole screen on every arrow
key / typed character — slow and flickery over a low-bandwidth link
like a serial console.

One-shot dialogs (show_confirm, _msg, _error) draw once and don't
loop-redraw, so they keep clear() as-is — no benefit to changing them
and it's one less thing to get wrong.

Also handle curses.KEY_RESIZE explicitly in the main list loop: force
one real clear() right after a detected resize, since erase()'s diff
against the pre-resize screen model isn't reliable across a genuine
dimension change. Screen-size changes were already picked up on the
next redraw before this (draw() runs unconditionally every loop
iteration), this just guarantees a clean repaint at the moment of the
actual resize instead of trusting the diff engine through it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 17:45:13 +03:00
Vlad Doloman
92d512f0d2 show email alongside CN in TUI main menu cert list
CertInfo.email was already populated from index.txt but never
rendered; add it as a fixed-width column next to CN in the cert
list rows (blank when unknown).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 16:43:29 +03:00
Vlad Doloman
f7ce56a6d4 fix curses.newwin() crash on narrow terminals in TUI dialogs
show_confirm() sized its window purely from message content, with no
upper bound — a long line (the Cryptgeon password URL in the
post-issuance "send email?" prompt) could easily exceed a narrow
terminal's width (e.g. a serial console), making curses.newwin()
raise "curses function returned NULL" and crash the whole app.
show_cert_form() had the same unguarded newwin() call, just with a
fixed 13x62 size instead of content-driven.

Clamp both windows' height/width to the actual screen size, and wrap
newwin() itself in try/except as a last-resort fallback (declined
confirm / cancelled form) in case clamping still isn't enough.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 16:33:38 +03:00
Vlad Doloman
6248ff47b8 add --list/--list-all CLI flags
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 16:22:22 +03:00
Vlad Doloman
3c4eef6bb0 add CliRunner.list_certs()
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 16:21:24 +03:00
Vlad Doloman
444e35f8cd add _format_cert_table() helper for CLI cert listings
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 16:20:41 +03:00
Vlad Doloman
17abe31650 fix invisible TUI cursor/buttons on serial consoles with broken color pairs
init_colors() now survives a terminal rejecting every init_pair() call
(some serial-console terminfo entries claim start_color()/COLORS support
but can't actually set custom pairs), but that exposed a follow-on bug:
the list cursor and the cert-form Continue/Cancel buttons indicated
selection purely via the COLOR_SELECTED color pair, so once that pair
silently no-ops the current row/button becomes indistinguishable from
an unselected one. Add curses.A_REVERSE alongside the color pair for
the selected state — a plain video attribute that doesn't depend on
custom-pair support, so the cursor stays visible either way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 16:07:50 +03:00
Vlad Doloman
4798ce3f18 fix TUI crash on serial consoles; make index.txt the sole email store
curses.use_default_colors() raises on terminals (e.g. serial consoles
using TERM=linux/vt100) whose terminfo lacks default-color support;
init_colors() now falls back to an explicit black background and caps
the "disabled" color to 8-color terminals.

--reissue without --email silently sent no mail when the CN wasn't in
openvpncertupdate-metadata.json, even though its email was already
embedded in the cert's index.txt subject (via EASYRSA_REQ_EMAIL) and
the TUI already fell back to it. Since build_client_full() always
writes that subject field whenever an email is known, metadata.json
was a redundant, driftable copy — remove it and have get_email() read
index.txt directly, picking the last non-revoked entry for a CN since
index.txt can contain revoked/duplicate lines for the same CN.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 15:46:10 +03:00
Vlad Doloman
bd17cdd68d fix Cryptgeon protocol and add CA passphrase / external config resolution
Cryptgeon's real protocol (verified against upstream occulto/frontend/backend
source) never matched what create_note() sent: it used a SHA-256-derived key
instead of the raw one, a single-blob ciphertext instead of the delimited
AES-GCM--nonce--ciphertext format, an empty meta field instead of a JSON
string, and a hash-bang URL instead of the real /note/<id>#<key> route -
notes uploaded fine but were undecryptable in the browser.

Also adds CA_PASSPHRASE auto-detection/prompting ("" auto-detects an
encrypted CA key and prompts, "!empty"/"!ask" opt out of/force the prompt)
and an external .conf file (--config / CONFIG_PATH / <script>.conf) that can
override the SETTINGS block without editing the script.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-06 12:58:21 +03:00
Vlad Doloman
f3e1471d1a feat: double Esc to exit main menu; fix Cryptgeon test for new payload schema
- First Esc in main screen shows "Press Esc again to quit" in footer;
  second Esc exits. Any other key resets the pending state.
- Update test_posts_to_correct_endpoint to expect contents as base64 string
  and presence of meta field, matching the updated Cryptgeon API payload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 15:31:12 +03:00
Vlad Doloman
24ee8fc347 fix: cursor visibility, Left/Right button nav, expand banned password chars
- Fix cursor not appearing in Certificate Details text fields (hint addstr
  was clobbering the cursor position set by place_cursor before refresh)
- Add Left/Right arrow key switching between Continue and Cancel buttons
- Expand password generation banned characters from oO01lI to oO01lIQ5S2Z8B
  (adds visually ambiguous pairs: Q/0, 5/S, 2/Z, 8/B)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 14:47:56 +03:00
Vlad Doloman
6ed7f9feef feat: add Continue and Cancel buttons to Certificate Details form
Tab (or Down/Enter on a field) now cycles through the three text fields
and then onto [ Continue ] and [ Cancel ] buttons at the bottom of the
form.  Enter or Space activates the focused button.  Ctrl-G still
confirms immediately from any position.  Escape still cancels.

The form window is trimmed from h=16 to h=13 since the buttons replace
the now-redundant empty space below the password field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 13:53:20 +03:00
Vlad Doloman
567c3e9759 fix: replace Ctrl-S confirm with Ctrl-G in Certificate Details form
Ctrl-S (0x13) is the XOFF flow-control character; terminals intercept it
before curses sees it, causing output suspension in Konsole and others.
Replaced with Ctrl-G (0x07 / BEL) which has no terminal-level meaning.
Updated hint bar and test stub accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 13:47:14 +03:00
Vlad Doloman
f0db11597a 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>
2026-06-24 12:33:25 +03:00
Vlad Doloman
6e4be8965d feat: add two-digit counter suffix to vpn-configs dir (CN_YYYY-MM-DD_NN) 2026-06-24 03:25:42 +03:00
Vlad Doloman
006cedebbe feat: --show-eml outputs base64-encoded .eml; implies --no-send-email by default 2026-06-24 03:02:38 +03:00
Vlad Doloman
996000c5ae feat: --send-email / --no-send-email flag for CLI mode 2026-06-24 02:54:48 +03:00
Vlad Doloman
ceaf24414a feat: CLI mode (--create, --reissue, --revoke, --gen-crl) 2026-06-24 02:53:47 +03:00
Vlad Doloman
1222fb46a4 feat: toggle all-certs view with A key (load_all_certs, Action.TOGGLE_ALL) 2026-06-24 02:36:32 +03:00
Vlad Doloman
ca6fe9f525 feat: TUI main selection screen with checkboxes, menu items, revoke hotkey 2026-06-24 00:20:10 +03:00
Vlad Doloman
565a152243 fix: spec compliance for TUI DIALOGS section
- Rename CertFormResult.cancelled → confirmed (True=submit, False=cancel/Esc)
- Change _FORM_FIELDS from list to tuple
- Update _FORM_LABELS to exact values: CN, Email, Password
- Update _FORM_FIELD_Y to {cn:3, email:5, password:7}
- Add password param to show_cert_form (uses generate_password() if empty)
- Add mask=True to password InputField (both init and F5-regen sites)
- Add ord("Q") to show_confirm false-return condition
- Add KEY_UP/KEY_DOWN navigation in show_cert_form
- Add Ctrl-S (0x13) as immediate submit key in show_cert_form
- Update tests/test_dialogs.py: confirmed=True on submit, confirmed=False on cancel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 00:13:46 +03:00
Vlad Doloman
fefec14929 feat: TUI modal dialogs (confirm Y/N, cert detail form)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 00:07:38 +03:00
Vlad Doloman
3a6d6aa125 feat: TUI widget primitives (colors, InputField, draw helpers)
Implement COLOR_* constants, initialization, box/centered drawing,
clamp utility, and InputField with handle_key/value logic for pure
text editing without live curses. All tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 00:00:52 +03:00
Vlad Doloman
a2593b644e feat: MIME email composer with .ovpn attachment via msmtp/sendmail
Add send_email() function that composes multipart MIME messages with
template-based body and .ovpn file attachment, piped to mail binary via
subprocess.Popen. Supports Cryptgeon URL insertion and sender metadata.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:58:00 +03:00
Vlad Doloman
5b9ee8d448 feat: Cryptgeon client with AES-GCM client-side encryption
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:55:32 +03:00
Vlad Doloman
8a93a1a727 feat: metadata store (CN→email) and .ovpn config builder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:52:55 +03:00
Vlad Doloman
c93a7b0c00 feat: EasyRSA subprocess wrappers (revoke, build, gen-crl, copy-crl)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:50:59 +03:00
Vlad Doloman
941b3d3f5b feat: password generator with 28-char charset rules
Implements generate_password() function that produces 28-character passwords
following charset rules: position 1 uppercase, position 2 and last lowercase
with restricted characters, middle positions alphanumeric. Uses secrets module
for cryptographic randomness.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:49:14 +03:00
Vlad Doloman
3618d3a73b feat: project skeleton + settings + PKI index.txt reader 2026-06-23 23:46:44 +03:00