diff --git a/requirements-all.txt b/requirements-all.txt index 0ce00c34be..056e344f34 100644 --- a/requirements-all.txt +++ b/requirements-all.txt @@ -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 diff --git a/requirements-test.txt b/requirements-test.txt index 20dde9b398..07a078359f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -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 diff --git a/resotocore/resotocore/web/certificate_handler.py b/resotocore/resotocore/web/certificate_handler.py index 5781a9b10a..e2047cd513 100644 --- a/resotocore/resotocore/web/certificate_handler.py +++ b/resotocore/resotocore/web/certificate_handler.py @@ -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 @@ -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() @@ -124,7 +130,9 @@ 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: @@ -132,6 +140,10 @@ def __create_client_context(config: CoreConfig, ca_cert: Certificate) -> SSLCont 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 @@ -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) @@ -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): @@ -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( @@ -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 @@ -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")) @@ -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() @@ -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) diff --git a/resotocore/tests/resotocore/web/certificate_handler_test.py b/resotocore/tests/resotocore/web/certificate_handler_test.py index 8be026753f..b2061b04b4 100644 --- a/resotocore/tests/resotocore/web/certificate_handler_test.py +++ b/resotocore/tests/resotocore/web/certificate_handler_test.py @@ -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, @@ -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")