Files
openvpncertupdate/CLAUDE.md
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

5.2 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

openvpncertupdate is a single-file Python + curses TUI tool for managing OpenVPN user certificates via EasyRSA 3.2.x. It lists expiring/expired certs, re-issues them with fresh keys, creates new certs, and delivers configs via Cryptgeon (one-time password URL) and email. It also supports a non-interactive CLI mode for scripting and cron use.

Running

pip install -r requirements.txt   # just: cryptography>=41
python3 openvpncertupdate.py               # interactive TUI
python3 openvpncertupdate.py --create CN --email user@example.com
python3 openvpncertupdate.py --reissue CN [--email user@example.com]
python3 openvpncertupdate.py --revoke CN
python3 openvpncertupdate.py --gen-crl

Edit the SETTINGS block at the top of openvpncertupdate.py before first run.

CLI flags

Flag Effect
--create CN Issue new cert (requires --email)
--reissue CN Revoke + reissue cert (--email optional, falls back to stored)
--revoke CN Revoke cert and regenerate CRL
--gen-crl Regenerate and copy CRL only
--email EMAIL Recipient address
--send-email Force email delivery
--no-send-email Skip email; print URL to stdout
--show-eml Print base64-encoded .eml to stdout (implies --no-send-email unless --send-email also given)
--config PATH External .conf file overriding SETTINGS (overrides CONFIG_PATH; missing file here is an error)

Tests

python3 -m pytest tests/ -v
python3 -m pytest tests/test_password.py -v            # single file
python3 -m pytest tests/test_pki.py::test_sorted_ascending -v  # single test

File layout — sections inside openvpncertupdate.py

Section Key symbols
SETTINGS all-caps constants
SETTINGS OVERRIDE ConfigError, load_settings_overrides(), _OVERRIDABLE_SETTINGS
PKI CertInfo, load_expiring_certs(), load_all_certs(), _parse_index_line()
PASSWORD generate_password()
EASYRSA EasyRSAError, revoke_issued(), build_client_full(), gen_crl(), copy_crl(), is_ca_key_encrypted(), resolve_ca_passphrase()
METADATA load_metadata(), save_email(), get_email()
CONFIG build_ovpn()vpn-configs/<CN>_<YYYY-MM-DD>_<NN>/CONFIG_NAME
CRYPTGEON CryptgeonError, create_note()
MAILER build_mime_message(), send_email()
TUI WIDGETS InputField, clamp(), draw_box(), init_colors(), COLOR_*
TUI DIALOGS show_confirm(), show_cert_form(), CertFormResult
TUI SCREEN show_main_screen(), Action, ScreenResult
APP CursesApp
CLI CliRunner, _build_parser()
ENTRY POINT main()

Re-issue workflow

  1. revoke-issued <CN> — archives old key + CSR to pki/revoked/
  2. build-client-full <CN> --passout=pass:<pw> — generates new key + cert
  3. CRL not auto-updated during renewal; use "Regenerate CRL" menu item or r hotkey

TUI key bindings

Key Action
/ Navigate list
Space Toggle checkbox selection
Enter Confirm / open selected item
r Revoke selected cert
A Toggle between expiring-only and all-certs view
q/Esc Quit

Key constraints

  • External config file (load_settings_overrides(), run once in main() right after arg parsing, before dispatch): resolution order is --config PATH > CONFIG_PATH setting > <this-script-path>.conf next to the script. The CLI flag or CONFIG_PATH make the path explicit — a missing file there is a fatal ConfigError; the default <script>.conf path is optional and silently skipped if absent. The file is executed as Python (same syntax as the SETTINGS block, so only run trusted files) and only names listed in _OVERRIDABLE_SETTINGS are applied — CONFIG_PATH itself is deliberately not overridable this way
  • Email: set SMTP_HOST to use smtplib (SMTP_TLS: "starttls"/"ssl"/""); leave empty to use MAIL_BINARY. Auth skipped when SMTP_USER=""
  • EasyRSA called with --batch; --passin=pass:<passphrase> omitted when the resolved passphrase is empty
  • CA passphrase resolution (resolve_ca_passphrase(), run once in main() right after arg parsing, before dispatch): CA_PASSPHRASE="" → auto-detect via is_ca_key_encrypted() (checks <PKI_DIR>/private/ca.key PEM header for ENCRYPTED) and prompt only if encrypted; "!empty" → never check/prompt, passphrase is ""; "!ask" → always prompt, skip detection; any other value → used literally
  • Cryptgeon: matches the occulto browser client — key=os.urandom(32) used directly (no derivation) for AES-256-GCM; contents = base64(b"AES-GCM") + "--" + base64(nonce) + "--" + base64(ciphertext); meta = JSON string {"type": "text"}; URL = <base>/note/<id>#<key.hex()>
  • copy_crl() does chmod 644 after copy
  • Password: pos 1=uppercase, pos 2=lowercase (no j), pos 3-27=alphanumeric, pos 28=lowercase (no j); oO01lIQ5S2Z8B banned everywhere
  • Inline file path: <PKI_DIR>/inline/private/<CN>.inline
  • User emails stored in <PKI_DIR>/openvpncertupdate-metadata.json