Skip to content

Commit 9cb0c47

Browse files
authored
Merge pull request #146 from pvarki/mtls_init_robustness
Make startup mTLS client cert creation more robust, update deps
2 parents 09e28ff + f93e0fe commit 9cb0c47

14 files changed

Lines changed: 697 additions & 659 deletions

File tree

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 1.12.0
2+
current_version = 1.12.1
33
commit = False
44
tag = False
55

poetry.lock

Lines changed: 572 additions & 546 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "rasenmaeher_api"
3-
version = "1.12.0"
3+
version = "1.12.1"
44
description = "python-rasenmaeher-api"
55
authors = [
66
"Aciid <703382+Aciid@users.noreply.github.com>",
@@ -96,7 +96,7 @@ brotli = "^1.0"
9696
cchardet = { version="^2.1", python="<=3.10"}
9797
filelock = "^3.12"
9898
python-keycloak = "^4.2.0"
99-
sqlmodel = ">=0.0.22,<1.0"
99+
sqlmodel = ">=0.0.37,<1.0"
100100
psycopg2-binary = "^2.9"
101101

102102
[tool.poetry.group.dev.dependencies]

src/rasenmaeher_api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""python-rasenmaeher-api"""
22

3-
__version__ = "1.12.0" # NOTE Use `bump2version --config-file patch` to bump versions correctly
3+
__version__ = "1.12.1" # NOTE Use `bump2version --config-file patch` to bump versions correctly
Lines changed: 1 addition & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1 @@
1-
"""Certificate operations module with configurable backend."""
2-
3-
# pylint: disable=duplicate-code # Re-exports same API from selected backend
4-
5-
from .errors import CertError, NoResult, ErrorResult, DBLocked, NoValue
6-
from ..rmsettings import RMSettings, CertBackend
7-
8-
_backend = RMSettings.singleton().cert_backend
9-
10-
if _backend == CertBackend.CERT_MANAGER:
11-
from .cert_manager import (
12-
get_ca,
13-
get_bundle,
14-
get_crl,
15-
get_ocsprest_crl,
16-
sign_csr,
17-
revoke_pem,
18-
revoke_serial,
19-
validate_reason,
20-
refresh_ocsp,
21-
dump_crlfiles,
22-
sign_ocsp,
23-
certadd_pem,
24-
anon_sign_csr,
25-
ReasonTypes,
26-
)
27-
elif _backend == CertBackend.CFSSL:
28-
from .cfssl import (
29-
get_ca,
30-
get_bundle,
31-
get_crl,
32-
get_ocsprest_crl,
33-
sign_csr,
34-
revoke_pem,
35-
revoke_serial,
36-
validate_reason,
37-
refresh_ocsp,
38-
dump_crlfiles,
39-
sign_ocsp,
40-
certadd_pem,
41-
anon_sign_csr,
42-
ReasonTypes,
43-
)
44-
else:
45-
raise ValueError(f"Unknown cert backend: {_backend}")
46-
47-
__all__ = [
48-
# Errors (always available)
49-
"CertError",
50-
"NoResult",
51-
"ErrorResult",
52-
"DBLocked",
53-
"NoValue",
54-
# Public functions
55-
"get_ca",
56-
"get_bundle",
57-
"get_crl",
58-
"get_ocsprest_crl",
59-
# Private functions
60-
"sign_csr",
61-
"revoke_pem",
62-
"revoke_serial",
63-
"validate_reason",
64-
"refresh_ocsp",
65-
"dump_crlfiles",
66-
"sign_ocsp",
67-
"certadd_pem",
68-
# Anonymous functions
69-
"anon_sign_csr",
70-
# Types
71-
"ReasonTypes",
72-
]
1+
"""Certificate handling backend plugins"""
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Certificate operations module with configurable backend."""
2+
3+
# pylint: disable=duplicate-code # Re-exports same API from selected backend
4+
5+
from .errors import CertError, NoResult, ErrorResult, DBLocked, NoValue
6+
from ..rmsettings import RMSettings, CertBackend
7+
8+
_backend = RMSettings.singleton().cert_backend
9+
10+
if _backend == CertBackend.CERT_MANAGER:
11+
from .cert_manager import (
12+
get_ca,
13+
get_bundle,
14+
get_crl,
15+
get_ocsprest_crl,
16+
sign_csr,
17+
revoke_pem,
18+
revoke_serial,
19+
validate_reason,
20+
refresh_ocsp,
21+
dump_crlfiles,
22+
sign_ocsp,
23+
certadd_pem,
24+
anon_sign_csr,
25+
ReasonTypes,
26+
)
27+
elif _backend == CertBackend.CFSSL:
28+
from .cfssl import (
29+
get_ca,
30+
get_bundle,
31+
get_crl,
32+
get_ocsprest_crl,
33+
sign_csr,
34+
revoke_pem,
35+
revoke_serial,
36+
validate_reason,
37+
refresh_ocsp,
38+
dump_crlfiles,
39+
sign_ocsp,
40+
certadd_pem,
41+
anon_sign_csr,
42+
ReasonTypes,
43+
)
44+
else:
45+
raise ValueError(f"Unknown cert backend: {_backend}")
46+
47+
__all__ = [
48+
# Errors (always available)
49+
"CertError",
50+
"NoResult",
51+
"ErrorResult",
52+
"DBLocked",
53+
"NoValue",
54+
# Public functions
55+
"get_ca",
56+
"get_bundle",
57+
"get_crl",
58+
"get_ocsprest_crl",
59+
# Private functions
60+
"sign_csr",
61+
"revoke_pem",
62+
"revoke_serial",
63+
"validate_reason",
64+
"refresh_ocsp",
65+
"dump_crlfiles",
66+
"sign_ocsp",
67+
"certadd_pem",
68+
# Anonymous functions
69+
"anon_sign_csr",
70+
# Types
71+
"ReasonTypes",
72+
]

src/rasenmaeher_api/db/people.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from .base import ORMBaseModel, utcnow
2424
from ..web.api.middleware.datatypes import MTLSorJWTPayload
2525
from .errors import NotFound, Deleted, BackendError, CallsignReserved
26-
from ..cert import sign_csr, revoke_pem, validate_reason, ReasonTypes, refresh_ocsp
26+
from ..cert.backend import sign_csr, revoke_pem, validate_reason, ReasonTypes, refresh_ocsp
2727
from ..productapihelpers import post_to_all_products
2828
from ..rmsettings import RMSettings
2929
from ..kchelpers import KCClient, KCUserData

src/rasenmaeher_api/mtlsinit.py

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Init mTLS client cert for RASENMAEHER itself"""
22

3+
from typing import Optional
34
import asyncio
45
from pathlib import Path
56
import logging
@@ -10,8 +11,8 @@
1011
import aiohttp
1112
import filelock
1213

13-
14-
from .rmsettings import switchme_to_singleton_call, RMSettings, CertBackend
14+
from .cert.errors import CertError
15+
from .rmsettings import RMSettings, CertBackend
1516

1617
LOGGER = logging.getLogger(__name__)
1718

@@ -39,26 +40,24 @@ async def _anon_sign_csr(csr: str) -> str:
3940
def check_settings_clientpaths() -> bool:
4041
"""Make sure the paths are defined, to defaults if needed, return True if setting was changed"""
4142
changed = False
42-
if not switchme_to_singleton_call.mtls_client_cert_path:
43-
switchme_to_singleton_call.mtls_client_cert_path = str(
44-
Path(switchme_to_singleton_call.persistent_data_dir) / "public" / f"{CERT_NAME_PREFIX}.pem"
45-
)
43+
config = RMSettings.singleton()
44+
if not config.mtls_client_cert_path:
45+
config.mtls_client_cert_path = str(Path(config.persistent_data_dir) / "public" / f"{CERT_NAME_PREFIX}.pem")
4646
changed = True
47-
if not switchme_to_singleton_call.mtls_client_key_path:
48-
switchme_to_singleton_call.mtls_client_key_path = str(
49-
Path(switchme_to_singleton_call.persistent_data_dir) / "private" / f"{CERT_NAME_PREFIX}.key"
50-
)
47+
if not config.mtls_client_key_path:
48+
config.mtls_client_key_path = str(Path(config.persistent_data_dir) / "private" / f"{CERT_NAME_PREFIX}.key")
5149
changed = True
5250
return changed
5351

5452

5553
def check_mtls_init() -> bool:
5654
"""Check if we have the cert and key"""
5755
check_settings_clientpaths()
58-
assert switchme_to_singleton_call.mtls_client_cert_path is not None
59-
assert switchme_to_singleton_call.mtls_client_key_path is not None
60-
cert_path = Path(switchme_to_singleton_call.mtls_client_cert_path)
61-
key_path = Path(switchme_to_singleton_call.mtls_client_key_path)
56+
config = RMSettings.singleton()
57+
assert config.mtls_client_cert_path is not None
58+
assert config.mtls_client_key_path is not None
59+
cert_path = Path(config.mtls_client_cert_path)
60+
key_path = Path(config.mtls_client_key_path)
6261
LOGGER.debug("cert_path={} exits={}".format(cert_path, cert_path.exists()))
6362
LOGGER.debug("key_path={} exits={}".format(key_path, key_path.exists()))
6463
if cert_path.exists() and key_path.exists():
@@ -71,45 +70,57 @@ async def mtls_init() -> None:
7170
if check_mtls_init():
7271
return
7372
privkeypath, pubkeypath, csrpath = resolve_filepaths(
74-
Path(switchme_to_singleton_call.persistent_data_dir), CERT_NAME_PREFIX
73+
Path(RMSettings.singleton().persistent_data_dir), CERT_NAME_PREFIX
7574
)
7675
check_settings_clientpaths()
77-
assert switchme_to_singleton_call.mtls_client_key_path is not None
78-
assert switchme_to_singleton_call.mtls_client_cert_path is not None
79-
if (pth := Path(switchme_to_singleton_call.mtls_client_key_path)) != privkeypath:
76+
config = RMSettings.singleton()
77+
assert config.mtls_client_key_path is not None
78+
assert config.mtls_client_cert_path is not None
79+
if (pth := Path(config.mtls_client_key_path)) != privkeypath:
8080
privkeypath = pth
8181
certpath = pubkeypath.parent / f"{CERT_NAME_PREFIX}.pem"
82-
if (pth := Path(switchme_to_singleton_call.mtls_client_cert_path)) != certpath:
82+
if (pth := Path(config.mtls_client_cert_path)) != certpath:
8383
certpath = pth
8484
lockpath = privkeypath.with_suffix(".lock")
8585
# Random sleep to avoid race conditions on these file accesses
8686
await asyncio.sleep(random.random() * 3.0) # nosec
8787
lock = filelock.FileLock(lockpath)
88+
csrpem: Optional[str] = None
8889
try:
8990
lock.acquire(timeout=0.0)
9091
# Check the privkey again to avoid overwriting.
91-
if privkeypath.exists():
92-
return None
93-
LOGGER.info("No mTLS client cert yet, creating it, this will take a moment")
94-
keypair = await async_create_keypair(privkeypath, pubkeypath)
95-
csrpem = await async_create_client_csr(keypair, csrpath, {"CN": switchme_to_singleton_call.mtls_client_cert_cn})
96-
certpem = (await _anon_sign_csr(csrpem)).replace("\\n", "\n")
92+
if not privkeypath.exists():
93+
LOGGER.info("No mTLS client cert yet, creating it, this will take a moment")
94+
keypair = await async_create_keypair(privkeypath, pubkeypath)
95+
LOGGER.debug("Creating mTLS client CSR")
96+
csrpem = await async_create_client_csr(keypair, csrpath, {"CN": config.mtls_client_cert_cn})
97+
if not certpath.exists():
98+
if not csrpem:
99+
LOGGER.debug("Loading mTLS client CSR from {}".format(csrpath))
100+
csrpem = csrpath.read_text()
101+
try:
102+
LOGGER.debug("Getting CSR signed")
103+
certpem = (await _anon_sign_csr(csrpem)).replace("\\n", "\n")
104+
LOGGER.debug("Saving mTLS cert to {}".format(certpath))
105+
certpath.write_text(certpem, encoding="ascii")
106+
except CertError as exc:
107+
LOGGER.exception("Signing failed: {}".format(exc))
97108
except filelock.Timeout:
98109
LOGGER.warning("Someone has already locked {}".format(lockpath))
99110
LOGGER.debug("Sleeping for ~5s and then recursing")
100111
await asyncio.sleep(5.0 + random.random()) # nosec
101112
return await mtls_init()
102113
finally:
103114
lock.release()
104-
certpath.write_text(certpem, encoding="ascii")
105115

106116

107117
async def get_session_winit() -> aiohttp.ClientSession:
108118
"""wrap libpvarki get_session to init checks"""
109119
await mtls_init()
110120
check_settings_clientpaths()
111-
assert switchme_to_singleton_call.mtls_client_cert_path is not None
112-
assert switchme_to_singleton_call.mtls_client_key_path is not None
113-
cert_path = Path(switchme_to_singleton_call.mtls_client_cert_path)
114-
key_path = Path(switchme_to_singleton_call.mtls_client_key_path)
121+
config = RMSettings.singleton()
122+
assert config.mtls_client_cert_path is not None
123+
assert config.mtls_client_key_path is not None
124+
cert_path = Path(config.mtls_client_cert_path)
125+
key_path = Path(config.mtls_client_key_path)
115126
return libsession((cert_path, key_path))

src/rasenmaeher_api/productapihelpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from .rmsettings import RMSettings
1414
from .mtlsinit import get_session_winit
15-
from .cert import refresh_ocsp
15+
from .cert.backend import refresh_ocsp
1616

1717
LOGGER = logging.getLogger(__name__)
1818

src/rasenmaeher_api/web/api/product/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from ....db.nonces import SeenToken
1919
from ....db.errors import NotFound
2020
from ....db import Person
21-
from ....cert import get_ca, get_bundle, sign_csr, revoke_pem, CertError
21+
from ....cert.backend import get_ca, get_bundle, sign_csr, revoke_pem, CertError
2222
from ....rmsettings import RMSettings
2323
from ....kchelpers import KCClient
2424
from ....productapihelpers import post_to_product

0 commit comments

Comments
 (0)