diff --git a/CHANGES/528.feature b/CHANGES/528.feature new file mode 100644 index 000000000..ca3629878 --- /dev/null +++ b/CHANGES/528.feature @@ -0,0 +1 @@ +Added support for syncing signatures using docker API extension. diff --git a/docs/workflows/sync.rst b/docs/workflows/sync.rst index d7cf90346..a3eb241de 100644 --- a/docs/workflows/sync.rst +++ b/docs/workflows/sync.rst @@ -66,6 +66,16 @@ Remote GET Response:: instead of the whole repository. Note that it is also possible to filter a bunch of tags that matches defined criteria by leveraging wildcards. +Some registries contain signed images. Such registries provide signatures in different ways. +If a registry provides signatures via a dedicated SigStore, a URL to it should be specified in +the ``sigstore`` field when creating a Remote. + +.. note:: + Some registries provide docker API extensions for ``atomic container signature`` type only, or + have ``cosign`` type signatures that are stored as a separate OCI artifact in a registry. + Pulp will automatically sync signatures provided via the docker API extension. At the moment, + `cosign` signatures are not supported. + Reference: `Container Remote Usage <../restapi.html#tag/Remotes:-Container>`_ diff --git a/pulp_container/app/registry_api.py b/pulp_container/app/registry_api.py index 584c37c71..78712e4f5 100644 --- a/pulp_container/app/registry_api.py +++ b/pulp_container/app/registry_api.py @@ -73,6 +73,7 @@ from pulp_container.app.utils import extract_data_from_signature from pulp_container.constants import ( EMPTY_BLOB, + SIGNATURE_API_EXTENSION_VERSION, SIGNATURE_HEADER, SIGNATURE_PAYLOAD_MAX_SIZE, SIGNATURE_TYPE, @@ -945,7 +946,7 @@ def get_response_data(signatures): data = [] for signature in signatures: signature = { - "schemaVersion": 2, + "schemaVersion": SIGNATURE_API_EXTENSION_VERSION, "type": signature.type, "name": signature.name, "content": signature.data, diff --git a/pulp_container/app/tasks/sync_stages.py b/pulp_container/app/tasks/sync_stages.py index a9a6983f3..6cb309e84 100644 --- a/pulp_container/app/tasks/sync_stages.py +++ b/pulp_container/app/tasks/sync_stages.py @@ -15,7 +15,13 @@ from pulpcore.plugin.stages import DeclarativeArtifact, DeclarativeContent, Stage from pulpcore.plugin.constants import TASK_STATES -from pulp_container.constants import MEDIA_TYPE, SIGNATURE_HEADER, SIGNATURE_SOURCE, SIGNATURE_TYPE +from pulp_container.constants import ( + MEDIA_TYPE, + SIGNATURE_API_EXTENSION_VERSION, + SIGNATURE_HEADER, + SIGNATURE_SOURCE, + SIGNATURE_TYPE, +) from pulp_container.app.models import ( Blob, BlobManifest, @@ -345,6 +351,29 @@ def _create_manifest_declarative_artifact(self, relative_url, saved_artifact, di ) return da + def _create_signature_declarative_content( + self, signature_raw, man_dc, name=None, signature_b64=None + ): + signature_json = extract_data_from_signature(signature_raw, man_dc.content.digest) + if signature_json is None: + return + + sig_digest = hashlib.sha256(signature_raw).hexdigest() + signature = ManifestSignature( + name=name or f"{man_dc.content.digest}@{sig_digest[:32]}", + digest=f"sha256:{sig_digest}", + type=SIGNATURE_TYPE.ATOMIC_SHORT, + key_id=signature_json["signing_key_id"], + timestamp=signature_json["signature_timestamp"], + creator=signature_json["optional"].get("creator"), + data=signature_b64 or base64.b64encode(signature_raw).decode(), + ) + sig_dc = DeclarativeContent( + content=signature, + extra_data={"signed_manifest_dc": man_dc}, + ) + return sig_dc + def create_manifest(self, list_dc, manifest_data): """ Create an Image Manifest from manifest data in a ManifestList. @@ -457,33 +486,38 @@ async def create_signatures(self, man_dc, signature_source): signature_raw = f.read() signature_counter += 1 - signature_json = extract_data_from_signature(signature_raw, man_dc.content.digest) - if signature_json is None: - continue - - sig_digest = hashlib.sha256(signature_raw).hexdigest() - signature = ManifestSignature( - name=f"{man_dc.content.digest}@{sig_digest[:32]}", - digest=f"sha256:{sig_digest}", - type=SIGNATURE_TYPE.ATOMIC_SHORT, - key_id=signature_json["signing_key_id"], - timestamp=signature_json["signature_timestamp"], - creator=signature_json["optional"].get("creator"), - data=base64.b64encode(signature_raw).decode(), - ) - sig_dc = DeclarativeContent( - content=signature, - extra_data={"signed_manifest_dc": man_dc}, - ) - signature_dcs.append(sig_dc) - - return signature_dcs + sig_dc = self._create_signature_declarative_content(signature_raw, man_dc) + if sig_dc: + signature_dcs.append(sig_dc) elif signature_source == SIGNATURE_SOURCE.API_EXTENSION: - # TODO in a PR for the extension support - pass + signatures_url = urlpath_sanitize( + self.remote.url, + "extensions/v2", + self.remote.upstream_name, + "signatures", + man_dc.content.digest, + ) + signatures_downloader = self.remote.get_downloader(url=signatures_url) + await signatures_downloader.run() + with open(signatures_downloader.path) as signatures_fd: + api_extension_signatures = json.loads(signatures_fd.read()) + for signature in api_extension_signatures.get("signatures", []): + if ( + signature.get("schemaVersion") == SIGNATURE_API_EXTENSION_VERSION + and signature.get("type") == SIGNATURE_TYPE.ATOMIC_SHORT + ): + signature_base64 = signature.get("content") + if signature_base64 is None: + continue + signature_raw = base64.b64decode(signature_base64) + sig_dc = self._create_signature_declarative_content( + signature_raw, man_dc, signature.get("name"), signature_base64 + ) + if sig_dc: + signature_dcs.append(sig_dc) - return [] + return signature_dcs def _include_layer(self, layer): """ diff --git a/pulp_container/constants.py b/pulp_container/constants.py index b03a276cb..2ca118eec 100644 --- a/pulp_container/constants.py +++ b/pulp_container/constants.py @@ -33,8 +33,9 @@ ATOMIC_SHORT="atomic", # short version is used in the JSON produced by API extension ) SIGNATURE_SOURCE = SimpleNamespace(SIGSTORE="sigstore", API_EXTENSION="API extension") - SIGNATURE_HEADER = "X-Registry-Supports-Signatures" MEGABYTE = 1_000_000 SIGNATURE_PAYLOAD_MAX_SIZE = 4 * MEGABYTE + +SIGNATURE_API_EXTENSION_VERSION = 2