from id_configurator.config import Configuration from id_configurator.context import Context from id_configurator.modules.certificates import ( CertificatesModule, _pem_name, _resolve_target_dir, ) CERT_CONTENT = "fake-cert-bytes" def make_config(with_certificate: bool) -> Configuration: data = { "uuid": "12345678-1234-5678-1234-567812345678", "organisation": "InterDiode", } if with_certificate: data["configurations"] = { "certificates": [{"name": "new-root-ca", "content": CERT_CONTENT}] } return Configuration.model_validate(data) def test_is_relevant_true_when_certificates_configured(tmp_path) -> None: context = Context(config=make_config(True), dst=tmp_path / "dst", src=None) assert CertificatesModule().is_relevant(context) is True def test_is_relevant_false_when_no_certificates(tmp_path) -> None: context = Context(config=make_config(False), dst=tmp_path / "dst", src=None) assert CertificatesModule().is_relevant(context) is False def test_apply_writes_certificate_and_rebuilds_bundle(tmp_path) -> None: dst = tmp_path / "dst" context = Context(config=make_config(True), dst=dst, src=None) CertificatesModule().apply(context) local_cert = dst / "usr/local/share/ca-certificates/new-root-ca.crt" assert local_cert.read_bytes() == b"fake-cert-bytes" bundle = dst / "etc/ssl/certs/ca-certificates.crt" assert bundle.read_bytes() == b"fake-cert-bytes\n" pem = dst / "etc/ssl/certs/new-root-ca.pem" assert pem.is_symlink() assert ( pem.readlink().as_posix() == "/usr/local/share/ca-certificates/new-root-ca.crt" ) openssl_default = dst / "usr/lib/ssl/cert.pem" assert openssl_default.is_symlink() assert openssl_default.readlink().as_posix() == "/etc/ssl/certs/ca-certificates.crt" def test_apply_sanitizes_certificate_name_with_slash(tmp_path) -> None: data = { "uuid": "12345678-1234-5678-1234-567812345678", "organisation": "InterDiode", "configurations": { "certificates": [{"name": "../../evil", "content": CERT_CONTENT}] }, } dst = tmp_path / "dst" context = Context(config=Configuration.model_validate(data), dst=dst, src=None) CertificatesModule().apply(context) local_cert = dst / "usr/local/share/ca-certificates/______evil.crt" assert local_cert.read_bytes() == b"fake-cert-bytes" # The "/" must never be interpreted as a path separator: without # sanitizing, "../../evil" would have reshuffled the write up to # usr/local/evil.crt instead of staying under ca-certificates/. assert not (dst / "usr/local/evil.crt").exists() def test_apply_merges_conf_listed_and_local_certificates(tmp_path) -> None: src = tmp_path / "src" local_certs_dir = src / "usr/local/share/ca-certificates" local_certs_dir.mkdir(parents=True) (local_certs_dir / "existing.crt").write_bytes(b"existing-bytes") (local_certs_dir / "new-root-ca.crt").write_bytes( b"stale-bytes-should-be-overwritten" ) system_certs_dir = src / "usr/share/ca-certificates" system_certs_dir.mkdir(parents=True) # No trailing newline, to exercise the newline-normalization on append. (system_certs_dir / "system-ca.crt").write_bytes(b"system-ca-bytes") (src / "etc/ca-certificates.conf").parent.mkdir(parents=True, exist_ok=True) (src / "etc/ca-certificates.conf").write_text( "# a comment\n\n!disabled-ca.crt\nsystem-ca.crt\n", encoding="utf-8" ) dst = tmp_path / "dst" context = Context(config=make_config(True), dst=dst, src=src) CertificatesModule().apply(context) local_dst = dst / "usr/local/share/ca-certificates" assert (local_dst / "existing.crt").read_bytes() == b"existing-bytes" assert (local_dst / "new-root-ca.crt").read_bytes() == b"fake-cert-bytes" bundle = dst / "etc/ssl/certs/ca-certificates.crt" # ca-certificates.conf entries first, then local certificates sorted by name. assert bundle.read_bytes() == ( b"system-ca-bytes\nexisting-bytes\nfake-cert-bytes\n" ) etc_certs = dst / "etc/ssl/certs" assert ( etc_certs / "system-ca.pem" ).readlink().as_posix() == "/usr/share/ca-certificates/system-ca.crt" assert ( etc_certs / "existing.pem" ).readlink().as_posix() == "/usr/local/share/ca-certificates/existing.crt" assert ( etc_certs / "new-root-ca.pem" ).readlink().as_posix() == "/usr/local/share/ca-certificates/new-root-ca.crt" # The disabled entry must not produce a symlink or appear in the bundle. assert not (etc_certs / "disabled-ca.pem").exists() def test_apply_warns_and_skips_missing_conf_entry(tmp_path, caplog) -> None: src = tmp_path / "src" (src / "usr/share/ca-certificates").mkdir(parents=True) (src / "etc/ca-certificates.conf").parent.mkdir(parents=True, exist_ok=True) (src / "etc/ca-certificates.conf").write_text("missing.crt\n", encoding="utf-8") dst = tmp_path / "dst" context = Context(config=make_config(True), dst=dst, src=src) with caplog.at_level("WARNING"): CertificatesModule().apply(context) assert "missing.crt" in caplog.text bundle = dst / "etc/ssl/certs/ca-certificates.crt" assert bundle.read_bytes() == b"fake-cert-bytes\n" def test_pem_name_sanitizes_special_characters() -> None: assert _pem_name("usr/share/ca-certificates/New Root, CA.crt") == "New_Root__CA.pem" assert _pem_name("Vendor (Test).crt") == "Vendor_=Test=.pem" def test_apply_skips_rhel_trust_store_when_absent(tmp_path) -> None: dst = tmp_path / "dst" context = Context(config=make_config(True), dst=dst, src=None) CertificatesModule().apply(context) assert not (dst / "etc/pki").exists() def test_apply_updates_rhel_trust_store_when_present(tmp_path) -> None: src = tmp_path / "src" anchors_dir = src / "etc/pki/ca-trust/source/anchors" anchors_dir.mkdir(parents=True) (anchors_dir / "existing-anchor.pem").write_bytes(b"existing-anchor-bytes") (anchors_dir / "new-root-ca.crt").write_bytes(b"stale-should-be-overwritten") extracted_bundle = src / "etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" extracted_bundle.parent.mkdir(parents=True) # No trailing newline, to exercise the newline-normalization on append. extracted_bundle.write_bytes(b"vendor-bundle-bytes") dst = tmp_path / "dst" context = Context(config=make_config(True), dst=dst, src=src) CertificatesModule().apply(context) anchors_dst = dst / "etc/pki/ca-trust/source/anchors" assert ( anchors_dst / "existing-anchor.pem" ).read_bytes() == b"existing-anchor-bytes" assert (anchors_dst / "new-root-ca.crt").read_bytes() == b"fake-cert-bytes" bundle_dst = dst / "etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" assert bundle_dst.read_bytes() == ( b"vendor-bundle-bytes\nexisting-anchor-bytes\nfake-cert-bytes\n" ) def test_apply_rebuilds_rhel_trust_store_without_preexisting_bundle(tmp_path) -> None: src = tmp_path / "src" (src / "etc/pki/ca-trust/source/anchors").mkdir(parents=True) dst = tmp_path / "dst" context = Context(config=make_config(True), dst=dst, src=src) CertificatesModule().apply(context) bundle_dst = dst / "etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" assert bundle_dst.read_bytes() == b"fake-cert-bytes\n" def test_resolve_target_dir_returns_unchanged_when_not_a_symlink(tmp_path) -> None: src = tmp_path / "src" (src / "etc/ssl/certs").mkdir(parents=True) context = Context(config=make_config(True), dst=tmp_path / "dst", src=src) assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/ssl/certs" def test_resolve_target_dir_returns_unchanged_without_src(tmp_path) -> None: context = Context(config=make_config(True), dst=tmp_path / "dst", src=None) assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/ssl/certs" def test_resolve_target_dir_follows_absolute_symlink(tmp_path) -> None: src = tmp_path / "src" (src / "etc/pki/tls/certs").mkdir(parents=True) (src / "etc/ssl").mkdir(parents=True) (src / "etc/ssl/certs").symlink_to("/etc/pki/tls/certs") context = Context(config=make_config(True), dst=tmp_path / "dst", src=src) assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/pki/tls/certs" def test_resolve_target_dir_follows_relative_symlink(tmp_path) -> None: src = tmp_path / "src" (src / "etc/pki/tls/certs").mkdir(parents=True) (src / "etc/ssl").mkdir(parents=True) (src / "etc/ssl/certs").symlink_to("../pki/tls/certs") context = Context(config=make_config(True), dst=tmp_path / "dst", src=src) assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/pki/tls/certs" def test_resolve_target_dir_falls_back_when_relative_symlink_escapes_src( tmp_path, ) -> None: src = tmp_path / "src" (src / "etc/ssl").mkdir(parents=True) # Escapes past itself via enough ".." components. (src / "etc/ssl/certs").symlink_to("../../../../../outside") context = Context(config=make_config(True), dst=tmp_path / "dst", src=src) assert _resolve_target_dir(context, "etc/ssl/certs") == "etc/ssl/certs" def test_apply_writes_bundle_to_rhel_style_symlinked_etccertsdir(tmp_path) -> None: src = tmp_path / "src" (src / "etc/pki/tls/certs").mkdir(parents=True) (src / "etc/ssl").mkdir(parents=True) (src / "etc/ssl/certs").symlink_to("/etc/pki/tls/certs") dst = tmp_path / "dst" context = Context(config=make_config(True), dst=dst, src=src) CertificatesModule().apply(context) assert not (dst / "etc/ssl/certs").exists() bundle = dst / "etc/pki/tls/certs/ca-certificates.crt" assert bundle.read_bytes() == b"fake-cert-bytes\n" pem = dst / "etc/pki/tls/certs/new-root-ca.pem" assert pem.is_symlink() # /usr/lib/ssl/cert.pem is Debian/Ubuntu-specific: not OpenSSL's default # CAfile on this (RHEL-style) system, so it must not be created. assert not (dst / "usr/lib/ssl/cert.pem").exists() def test_apply_is_idempotent_when_src_and_dst_are_the_same_live_root( tmp_path, ) -> None: # Regression test: applying directly to a live root (--src / --dst /, # as the Ansible role does) where /usr/lib/ssl/cert.pem already exists # (as it would on any host with ca-certificates already installed) must # not resolve that pre-existing symlink away when recreating it - doing # so would unlink/replace the bundle it points to instead, corrupting it # into a symlink to itself and breaking every subsequent run. root = tmp_path / "root" etc_ssl_certs = root / "etc/ssl/certs" etc_ssl_certs.mkdir(parents=True) (etc_ssl_certs / "ca-certificates.crt").write_bytes(b"preexisting-bundle-bytes") usr_lib_ssl = root / "usr/lib/ssl" usr_lib_ssl.mkdir(parents=True) (usr_lib_ssl / "cert.pem").symlink_to("/etc/ssl/certs/ca-certificates.crt") context = Context(config=make_config(True), dst=root, src=root) for _ in range(2): CertificatesModule().apply(context) bundle = etc_ssl_certs / "ca-certificates.crt" assert not bundle.is_symlink() assert bundle.read_bytes() == b"fake-cert-bytes\n" openssl_default = usr_lib_ssl / "cert.pem" assert openssl_default.is_symlink() assert ( openssl_default.readlink().as_posix() == "/etc/ssl/certs/ca-certificates.crt" )