diff --git a/sources/org.osbuild.curl b/sources/org.osbuild.curl index 086c97ba..5967867b 100755 --- a/sources/org.osbuild.curl +++ b/sources/org.osbuild.curl @@ -2,13 +2,19 @@ """ Source for downloading files from URLs. -The files are indexed by their content hash. Can download files -that require secrets. The only secret provider currently supported -is `org.osbuild.rhsm` for downloading Red Hat content that requires -a subscriptions. +The files are indexed by their content hash. It can download files +that require secrets. The secret providers currently supported are: -Internally use curl to download the files; the files are cached in -an internal cache. Multiple parallel connections are used to speed +- `org.osbuild.rhsm` for downloading Red Hat content that requires + a subscriptions. +- `org.osbuild.mtls` for downloading content that requires client + certificats. The paths to the key and cert should be set in the + environment in OSBUILD_SOURCES_CURL_SSL_CLIENT_KEY, + OSBUILD_SOURCES_CURL_SSL_CLIENT_CERT, and optionally + OSBUILD_SOURCES_CURL_SSL_CA_CERT. + +It uses curl to download the files; the files are cached in an +internal cache. Multiple parallel connections are used to speed up the download. """ @@ -107,6 +113,16 @@ class CurlSource(sources.SourceService): if self.subscriptions is None: self.subscriptions = Subscriptions.from_host_system() url["secrets"] = self.subscriptions.get_secrets(url.get("url")) + elif url.get("secrets", {}).get("name") == "org.osbuild.mtls": + key = os.getenv("OSBUILD_SOURCES_CURL_SSL_CLIENT_KEY") + cert = os.getenv("OSBUILD_SOURCES_CURL_SSL_CLIENT_CERT") + if not (key and cert): + raise RuntimeError(f"mtls secrets required but key ({key}) or cert ({cert}) not defined") + url["secrets"] = { + 'ssl_ca_cert': os.getenv("OSBUILD_SOURCES_CURL_SSL_CA_CERT"), + 'ssl_client_cert': cert, + 'ssl_client_key': key, + } return checksum, url diff --git a/sources/test/test_curl_source.py b/sources/test/test_curl_source.py new file mode 100644 index 00000000..8ac4c8fd --- /dev/null +++ b/sources/test/test_curl_source.py @@ -0,0 +1,82 @@ +#!/usr/bin/python3 + +import contextlib +import os +import pathlib +import socket +import tempfile + +import pytest + +SOURCES_NAME = "org.osbuild.curl" + + +def test_curl_source_not_exists(sources_module): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + curl_source = sources_module.CurlSource.from_args(["--service-fd", str(sock.fileno())]) + tmpdir = tempfile.TemporaryDirectory() + curl_source.cache = tmpdir.name + desc = { + "url": "http://localhost:80/a", + } + checksum = "sha256:1234567890123456789012345678901234567890909b14ffb032aa20fa23d9ad6" + assert not curl_source.exists(checksum, desc) + + +def test_curl_source_exists(sources_module): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + curl_source = sources_module.CurlSource.from_args(["--service-fd", str(sock.fileno())]) + tmpdir = tempfile.TemporaryDirectory() + curl_source.cache = tmpdir.name + desc = { + "url": "http://localhost:80/a", + } + checksum = "sha256:1234567890123456789012345678901234567890909b14ffb032aa20fa23d9ad6" + pathlib.Path(os.path.join(tmpdir.name, checksum)).touch() + assert curl_source.exists(checksum, desc) + + +def test_curl_source_transform(sources_module): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + curl_source = sources_module.CurlSource.from_args(["--service-fd", str(sock.fileno())]) + tmpdir = tempfile.TemporaryDirectory() + curl_source.cache = tmpdir.name + desc = { + "url": "http://localhost:80/a", + "secrets": { + "name": "org.osbuild.mtls", + }, + } + + with contextlib.ExitStack() as cm: + os.environ["OSBUILD_SOURCES_CURL_SSL_CLIENT_KEY"] = "key" + os.environ["OSBUILD_SOURCES_CURL_SSL_CLIENT_CERT"] = "cert" + + def cb(): + del os.environ["OSBUILD_SOURCES_CURL_SSL_CLIENT_KEY"] + del os.environ["OSBUILD_SOURCES_CURL_SSL_CLIENT_CERT"] + cm.callback(cb) + checksum = "sha256:1234567890123456789012345678901234567890909b14ffb032aa20fa23d9ad6" + pathlib.Path(os.path.join(tmpdir.name, checksum)).touch() + new_desc = curl_source.transform(checksum, desc) + assert new_desc[1]["secrets"]["ssl_client_key"] == "key" + assert new_desc[1]["secrets"]["ssl_client_cert"] == "cert" + assert new_desc[1]["secrets"]["ssl_ca_cert"] is None + + +def test_curl_source_transform_fail(sources_module): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + curl_source = sources_module.CurlSource.from_args(["--service-fd", str(sock.fileno())]) + tmpdir = tempfile.TemporaryDirectory() + curl_source.cache = tmpdir.name + desc = { + "url": "http://localhost:80/a", + "secrets": { + "name": "org.osbuild.mtls", + }, + } + checksum = "sha256:1234567890123456789012345678901234567890909b14ffb032aa20fa23d9ad6" + pathlib.Path(os.path.join(tmpdir.name, checksum)).touch() + with pytest.raises(RuntimeError) as exc: + curl_source.transform(checksum, desc) + assert "mtls secrets required" in str(exc)