Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[resotocore][feat] --ca-cert is added to the client context #1792

Merged
merged 5 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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 @@
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 @@
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)

Check warning on line 193 in resotocore/resotocore/web/certificate_handler.py

View check run for this annotation

Codecov / codecov/patch

resotocore/resotocore/web/certificate_handler.py#L192-L193

Added lines #L192 - L193 were not covered by tests


class CertificateHandlerWithCA(CertificateHandler):
Expand All @@ -194,8 +207,9 @@
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 @@
# 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)

Check warning on line 274 in resotocore/resotocore/web/certificate_handler.py

View check run for this annotation

Codecov / codecov/patch

resotocore/resotocore/web/certificate_handler.py#L274

Added line #L274 was not covered by tests
else:
host_key, host_cert = CertificateHandlerWithCA._create_host_certificate(
config.api.host_certificate, ca_key, ca_cert
Expand All @@ -268,6 +282,7 @@
# 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 @@
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 @@
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")
Loading