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>
This commit is contained in:
@@ -2,6 +2,7 @@ import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from openvpncertupdate import (
|
||||
revoke_issued, build_client_full, gen_crl, copy_crl, EasyRSAError,
|
||||
is_ca_key_encrypted, resolve_ca_passphrase,
|
||||
)
|
||||
|
||||
|
||||
@@ -65,3 +66,75 @@ def test_copy_crl_copies_and_chmods(mock_chmod, mock_copy):
|
||||
copy_crl("/pki", "/etc/openvpn/crl.pem")
|
||||
mock_copy.assert_called_once_with("/pki/crl.pem", "/etc/openvpn/crl.pem")
|
||||
mock_chmod.assert_called_once_with("/etc/openvpn/crl.pem", 0o644)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# is_ca_key_encrypted
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _write_ca_key(pki_dir, content):
|
||||
private = pki_dir / "private"
|
||||
private.mkdir(parents=True, exist_ok=True)
|
||||
(private / "ca.key").write_text(content)
|
||||
|
||||
|
||||
def test_is_ca_key_encrypted_true_for_pkcs8_encrypted_header(tmp_path):
|
||||
_write_ca_key(tmp_path, "-----BEGIN ENCRYPTED PRIVATE KEY-----\nAAA\n-----END ENCRYPTED PRIVATE KEY-----\n")
|
||||
assert is_ca_key_encrypted(str(tmp_path)) is True
|
||||
|
||||
|
||||
def test_is_ca_key_encrypted_true_for_legacy_proc_type_header(tmp_path):
|
||||
_write_ca_key(tmp_path, "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,...\n\nAAA\n-----END RSA PRIVATE KEY-----\n")
|
||||
assert is_ca_key_encrypted(str(tmp_path)) is True
|
||||
|
||||
|
||||
def test_is_ca_key_encrypted_false_for_plain_key(tmp_path):
|
||||
_write_ca_key(tmp_path, "-----BEGIN PRIVATE KEY-----\nAAA\n-----END PRIVATE KEY-----\n")
|
||||
assert is_ca_key_encrypted(str(tmp_path)) is False
|
||||
|
||||
|
||||
def test_is_ca_key_encrypted_false_when_key_missing(tmp_path):
|
||||
assert is_ca_key_encrypted(str(tmp_path)) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# resolve_ca_passphrase
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_resolve_empty_string_prompts_when_ca_is_encrypted(tmp_path):
|
||||
_write_ca_key(tmp_path, "-----BEGIN ENCRYPTED PRIVATE KEY-----\nAAA\n-----END ENCRYPTED PRIVATE KEY-----\n")
|
||||
prompt = MagicMock(return_value="typed-pass")
|
||||
result = resolve_ca_passphrase("", str(tmp_path), prompt=prompt)
|
||||
assert result == "typed-pass"
|
||||
prompt.assert_called_once()
|
||||
|
||||
|
||||
def test_resolve_empty_string_skips_prompt_when_ca_not_encrypted(tmp_path):
|
||||
_write_ca_key(tmp_path, "-----BEGIN PRIVATE KEY-----\nAAA\n-----END PRIVATE KEY-----\n")
|
||||
prompt = MagicMock()
|
||||
result = resolve_ca_passphrase("", str(tmp_path), prompt=prompt)
|
||||
assert result == ""
|
||||
prompt.assert_not_called()
|
||||
|
||||
|
||||
def test_resolve_empty_sentinel_never_checks_or_prompts(tmp_path):
|
||||
_write_ca_key(tmp_path, "-----BEGIN ENCRYPTED PRIVATE KEY-----\nAAA\n-----END ENCRYPTED PRIVATE KEY-----\n")
|
||||
prompt = MagicMock()
|
||||
result = resolve_ca_passphrase("!empty", str(tmp_path), prompt=prompt)
|
||||
assert result == ""
|
||||
prompt.assert_not_called()
|
||||
|
||||
|
||||
def test_resolve_ask_sentinel_always_prompts_without_checking(tmp_path):
|
||||
# no ca.key on disk at all -- must not be checked/read for "!ask"
|
||||
prompt = MagicMock(return_value="typed-pass")
|
||||
result = resolve_ca_passphrase("!ask", str(tmp_path), prompt=prompt)
|
||||
assert result == "typed-pass"
|
||||
prompt.assert_called_once()
|
||||
|
||||
|
||||
def test_resolve_literal_passphrase_passed_through_unchanged(tmp_path):
|
||||
prompt = MagicMock()
|
||||
result = resolve_ca_passphrase("mysecret", str(tmp_path), prompt=prompt)
|
||||
assert result == "mysecret"
|
||||
prompt.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user