Skip to content

Commit

Permalink
[resotocore][feat] --ca-cert is added to the client context (#1792)
Browse files Browse the repository at this point in the history
  • Loading branch information
aquamatthias authored Oct 2, 2023
1 parent a458c23 commit f6ac06e
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 14 deletions.
3 changes: 3 additions & 0 deletions requirements-all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,6 @@ wrapt==1.15.0
yarl==1.9.2
zc-lockfile==3.0.post1
zipp==3.17.0

# FIX: 2.31.0.7 depends on urllib3>=2 which is wrong
types-requests==2.31.0.6
3 changes: 3 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,6 @@ wrapt==1.15.0
yarl==1.9.2
zc-lockfile==3.0.post1
zipp==3.17.0

# FIX: 2.31.0.7 depends on urllib3>=2 which is wrong
types-requests==2.31.0.6
41 changes: 28 additions & 13 deletions resotocore/resotocore/web/certificate_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@

class CertificateHandler(Service):
def __init__(
self, config: CoreConfig, ca_cert: Certificate, host_key: RSAPrivateKey, host_cert: Certificate, temp_dir: Path
self,
config: CoreConfig,
ca_cert: Certificate,
host_key: RSAPrivateKey,
host_cert: Certificate,
temp_dir: Path,
additional_trusted_authorities: Optional[List[Certificate]] = None,
) -> None:
super().__init__()
self.config = config
Expand All @@ -53,7 +59,7 @@ def __init__(
self.__recreate_ca_file() # write the CA bundle to the temp dir
self._ca_cert_recreate = Periodic("recreate ca bundle file", self.__recreate_ca_file, timedelta(hours=1))
self._host_context = self._create_host_context(config, self._host_cert, self._host_key)
self._client_context = self.__create_client_context(config, ca_cert)
self._client_context = self.__create_client_context(config, ca_cert, additional_trusted_authorities)

async def start(self) -> None:
await self._ca_cert_recreate.start()
Expand Down Expand Up @@ -124,14 +130,20 @@ def _create_host_context(config: CoreConfig, host_cert: Certificate, host_key: R
return ctx

@staticmethod
def __create_client_context(config: CoreConfig, ca_cert: Certificate) -> SSLContext:
def __create_client_context(
config: CoreConfig, ca_cert: Certificate, additional_trusted_authorities: Optional[List[Certificate]]
) -> SSLContext:
# noinspection PyTypeChecker
ctx = create_default_context(purpose=Purpose.SERVER_AUTH)
if config.args.ca_cert:
ctx.load_verify_locations(cafile=config.args.ca_cert)
else:
ca_bytes = cert_to_bytes(ca_cert).decode("utf-8")
ctx.load_verify_locations(cadata=ca_bytes)
# also load all additional trusted authorities into context
for cert in additional_trusted_authorities or []:
ca_bytes = cert_to_bytes(cert).decode("utf-8")
ctx.load_verify_locations(cadata=ca_bytes)
return ctx


Expand All @@ -152,10 +164,10 @@ def lookup(config: CoreConfig, temp_dir: Path) -> CertificateHandlerNoCA:
args = config.args

# if we get a ca certificate from the command line, use it
if args.ca_cert and args.ca_cert_key and args.cert and args.cert_key:
ca_cert = load_cert_from_file(args.ca_cert_cert)
host_key = load_key_from_file(args.ca_cert_key, args.ca_cert_key_pass)
host_cert = load_cert_from_file(args.ca_cert_cert)
if args.ca_cert and args.cert and args.cert_key:
ca_cert = load_cert_from_file(args.ca_cert)
host_key = load_key_from_file(args.cert_key, args.cert_key_pass)
host_cert = load_cert_from_file(args.cert)
log.info(f"Using CA certificate from command line. fingerprint:{cert_fingerprint(ca_cert)}")
return CertificateHandlerNoCA(config, ca_cert, host_key, host_cert, temp_dir)

Expand All @@ -177,7 +189,8 @@ def lookup(config: CoreConfig, temp_dir: Path) -> CertificateHandlerNoCA:
psk=config.args.psk,
)
tls_data.load()
return CertificateHandlerNoCA(config, tls_data.ca_cert, tls_data.key, tls_data.cert, temp_dir)
authorities = [load_cert_from_file(args.ca_cert)] if args.ca_cert else []
return CertificateHandlerNoCA(config, tls_data.ca_cert, tls_data.key, tls_data.cert, temp_dir, authorities)


class CertificateHandlerWithCA(CertificateHandler):
Expand All @@ -194,8 +207,9 @@ def __init__(
host_key: RSAPrivateKey,
host_cert: Certificate,
temp_dir: Path,
additional_trusted_authorities: Optional[List[Certificate]] = None,
) -> None:
super().__init__(config, ca_cert, host_key, host_cert, temp_dir)
super().__init__(config, ca_cert, host_key, host_cert, temp_dir, additional_trusted_authorities)
self._ca_key = ca_key

def create_key_and_cert(
Expand Down Expand Up @@ -254,10 +268,10 @@ def lookup(config: CoreConfig, db: StandardDatabase, temp_dir: Path) -> Certific
# if we get a ca certificate from the command line, use it
if args.ca_cert and args.ca_cert_key:
ca_key = load_key_from_file(args.ca_cert_key, args.ca_cert_key_pass)
ca_cert = load_cert_from_file(args.ca_cert_cert)
ca_cert = load_cert_from_file(args.ca_cert)
if args.cert and args.cert_key:
host_key = load_key_from_file(args.ca_cert_key, args.ca_cert_key_pass)
host_cert = load_cert_from_file(args.ca_cert_cert)
host_cert = load_cert_from_file(args.ca_cert)
else:
host_key, host_cert = CertificateHandlerWithCA._create_host_certificate(
config.api.host_certificate, ca_key, ca_cert
Expand All @@ -268,6 +282,7 @@ def lookup(config: CoreConfig, db: StandardDatabase, temp_dir: Path) -> Certific
# otherwise, load from database or create it
sd = db.collection("system_data")
maybe_ca: Optional[Json] = sd.get("ca") # type: ignore
authorities = [load_cert_from_file(args.ca_cert)] if args.ca_cert else []
if maybe_ca and isinstance(maybe_ca.get("key"), str) and isinstance(maybe_ca.get("certificate"), str):
log.debug("Found existing certificate in data store.")
key = load_key_from_bytes(maybe_ca["key"].encode("utf-8"))
Expand All @@ -276,7 +291,7 @@ def lookup(config: CoreConfig, db: StandardDatabase, temp_dir: Path) -> Certific
host_key, host_cert = CertificateHandlerWithCA._create_host_certificate(
config.api.host_certificate, key, certificate
)
return CertificateHandlerWithCA(config, key, certificate, host_key, host_cert, temp_dir)
return CertificateHandlerWithCA(config, key, certificate, host_key, host_cert, temp_dir, authorities)
else:
wo = "with" if args.ca_cert_key_pass else "without"
key, certificate = bootstrap_ca()
Expand All @@ -289,4 +304,4 @@ def lookup(config: CoreConfig, db: StandardDatabase, temp_dir: Path) -> Certific
host_key, host_cert = CertificateHandlerWithCA._create_host_certificate(
config.api.host_certificate, key, certificate
)
return CertificateHandlerWithCA(config, key, certificate, host_key, host_cert, temp_dir)
return CertificateHandlerWithCA(config, key, certificate, host_key, host_cert, temp_dir, authorities)
42 changes: 41 additions & 1 deletion resotocore/tests/resotocore/web/certificate_handler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from resotocore.core_config import CoreConfig
from resotocore.system_start import empty_config, parse_args
from resotocore.types import Json
from resotocore.web.certificate_handler import CertificateHandler, CertificateHandlerWithCA
from resotocore.web.certificate_handler import CertificateHandler, CertificateHandlerWithCA, CertificateHandlerNoCA
from resotolib.x509 import (
load_cert_from_bytes,
cert_fingerprint,
Expand Down Expand Up @@ -63,3 +63,43 @@ def test_load_from_args(default_config: CoreConfig) -> None:
config = evolve(default_config, args=args)
context = CertificateHandler._create_host_context(config, cert, pk)
assert context is not None


def test_additional_authorities(test_db: StandardDatabase) -> None:
config = empty_config()
ca_key, ca_cert = bootstrap_ca(common_name="the ca")
another_key, another_ca = bootstrap_ca(common_name="another ca")
key, cert = CertificateHandlerWithCA._create_host_certificate(config.api.host_certificate, ca_key, ca_cert)

def assert_certs(handler: CertificateHandler, name: str) -> None:
ca_crts = {crt["issuer"]: crt for crt in handler.client_context.get_ca_certs()}
assert ((("organizationName", "Some Engineering Inc."),), (("commonName", name),)) in ca_crts

with TemporaryDirectory() as temp:
ca_path = temp + "/ca.crt"
key_path = temp + "/ca.key"
another_ca_path = temp + "/another_ca.crt"
another_key_path = temp + "/another_ca.key"
write_cert_to_file(ca_cert, ca_path)
write_key_to_file(ca_key, key_path)
write_cert_to_file(another_ca, another_ca_path)
write_key_to_file(another_key, another_key_path)

# another ca is added explicitly
ca = CertificateHandlerWithCA(config, ca_key, ca_cert, key, cert, Path(temp), [another_ca])
assert_certs(ca, "another ca")
no_ca = CertificateHandlerNoCA(config, ca_cert, key, cert, Path(temp), [another_ca])
assert_certs(no_ca, "another ca")

# another ca is defined on the command line (no key)
config = evolve(config, args=parse_args(["--ca-cert", another_ca_path]))
assert_certs(CertificateHandlerWithCA.lookup(config, test_db, Path(temp)), "another ca")

# in case cert and key are defined, it is used as the ca
config = evolve(config, args=parse_args(["--ca-cert", ca_path, "--ca-cert-key", key_path]))
assert_certs(CertificateHandlerWithCA.lookup(config, test_db, Path(temp)), "the ca")

# get ca certificate and host certificate/license from args
args = ["--ca-cert", ca_path, "--cert", another_ca_path, "--cert-key", another_key_path]
config = evolve(config, args=parse_args(args))
assert_certs(CertificateHandlerNoCA.lookup(config, Path(temp)), "the ca")

0 comments on commit f6ac06e

Please sign in to comment.