-
Notifications
You must be signed in to change notification settings - Fork 52
Add referenced-image signing to sign_existing_releases hack #3313
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,22 +1,39 @@ | ||||||||||||||||||||||||||||||||
| #!/usr/bin/env python3 | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| Standalone tool to sign existing release images with Sigstore/cosign. | ||||||||||||||||||||||||||||||||
| Standalone tool to sign existing release images (and optionally the component | ||||||||||||||||||||||||||||||||
| images they reference) with Sigstore/cosign. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| This tool can sign release payloads (manifest lists or single manifests) that | ||||||||||||||||||||||||||||||||
| already exist in quay.io. It reuses the SigstoreSignatory class for signing logic. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| By default, only TAG-BASED signatures are created (digest signatures are skipped). | ||||||||||||||||||||||||||||||||
| This is appropriate for retroactive signing where digest signatures already exist. | ||||||||||||||||||||||||||||||||
| For release images, only TAG-BASED signatures are created by default (digest | ||||||||||||||||||||||||||||||||
| signatures are skipped). This is appropriate for retroactive signing where digest | ||||||||||||||||||||||||||||||||
| signatures already exist. Use --sign-digest to also create digest signatures. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Use --sign-release to control what gets signed: | ||||||||||||||||||||||||||||||||
| yes (default) sign the release image(s) and the components they reference | ||||||||||||||||||||||||||||||||
| only sign only the release image(s) | ||||||||||||||||||||||||||||||||
| no sign only the referenced component images | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Referenced component images are discovered by spidering each release payload with | ||||||||||||||||||||||||||||||||
| `oc adm release info -o json` and are always signed with digest identity only. | ||||||||||||||||||||||||||||||||
| NOTE: `oc adm release info` on a `-multi` pullspec only returns one arch's | ||||||||||||||||||||||||||||||||
| references, so to sign all referenced images across every architecture, pass the | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't true, multi referenced images are themselves are themselves manifest lists. So pick one arch, get the referenced manifest lists, and then head out to the single-arch shards to sign. $ oc adm release info -o json quay.io/openshift-release-dev/ocp-release:4.20.21-multi | jq -r '.references.spec.tags[] | .name + " " + .from.name' | head -n3
agent-installer-api-server quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:538d13386a5849c0316b2c8b81cd8d926d9905089a9d772235dd8e53d1cc4e3e
agent-installer-csr-approver quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:c30f11f320c06be1fc5e257ce3220c24ff5686fbdd041cd730074b9e6a01b9cb
agent-installer-node-agent quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:e10014a7a9f8e83f37fbafe2125fa4ac76fd9c58a58954bddcf8017622a901eb
$ oc image info quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:538d13386a5849c0316b2c8b81cd8d926d9905089a9d772235dd8e53d1cc4e3e
error: the image is a manifest list and contains multiple images - use --filter-by-os to select from:
OS DIGEST
linux/amd64 sha256:b9f2059776e64d8f25f041ac8742c4610267ae68d6c302250eb7ca1a81919209
linux/arm64 sha256:2ec9ebe0000be90e7aa4f02ea47ef618e8779549a1ddb72ad7e76e6392d35dcb
linux/s390x sha256:1289d310ea5ffc72752e588feb677ec42630422f4ce785b8d34d91ae02ed4cb3
linux/ppc64le sha256:870d49e712a92e0a6873686c23164f16ad983d8f79d9b9b918c639272230ac44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pinning this down more exactly, by explicitly comparing two shards of that release image: $ oc image info quay.io/openshift-release-dev/ocp-release:4.20.21-multi
error: the image is a manifest list and contains multiple images - use --filter-by-os to select from:
OS DIGEST
linux/amd64 sha256:5a55ef5c98fa4e0bfea201652cb8e779285202dbc36f1723f8553d294fe852a7
linux/arm64 sha256:5debfb941fbad45596b5665529d77377a8cb0d9a103be0a3c11ddf652ef557fe
linux/s390x sha256:bfd0f80bdfe9f6387f8092b132ac96e1afcc06240bb0aacd366ed9e8b2e437fd
linux/ppc64le sha256:9fb326aacc7daa7043c2be9f6b2048eaddedf266e6927525ad9a6378621b9d74
$ diff -u1 <(oc adm release info -o json quay.io/openshift-release-dev/ocp-release@sha256:5a55ef5c98fa4e0bfea201652cb8e779285202dbc36f1723f8553d294fe852a7 | jq -r '.references.spec.tags[] | .name + " " + .from.name') <(oc adm release info -o json quay.io/openshift-release-dev/ocp-release@sha256:5debfb941fbad45596b5665529d77377a8cb0d9a103be0a3c11ddf652ef557fe | jq -r '.references.spec.tags[] | .name + " " + .from.name')
...no difference...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for catching this! |
||||||||||||||||||||||||||||||||
| per-arch release pullspecs (e.g. via --file), not just the `-multi` pullspec. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Usage: | ||||||||||||||||||||||||||||||||
| # Sign a single release image (from art-tools directory) | ||||||||||||||||||||||||||||||||
| # Sign a single release image and its referenced components (from art-tools directory) | ||||||||||||||||||||||||||||||||
| uv run pyartcd/hack/sign_existing_releases.py --dry-run \ | ||||||||||||||||||||||||||||||||
| quay.io/openshift-release-dev/ocp-release:4.16.1-multi | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Sign multiple release images from a file | ||||||||||||||||||||||||||||||||
| uv run pyartcd/hack/sign_existing_releases.py --dry-run \ | ||||||||||||||||||||||||||||||||
| # Sign only the release images (skip components) from a file | ||||||||||||||||||||||||||||||||
| uv run pyartcd/hack/sign_existing_releases.py --dry-run --sign-release only \ | ||||||||||||||||||||||||||||||||
| --file pullspecs.txt | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Sign only the referenced component images | ||||||||||||||||||||||||||||||||
| uv run pyartcd/hack/sign_existing_releases.py --dry-run --sign-release no \ | ||||||||||||||||||||||||||||||||
| quay.io/openshift-release-dev/ocp-release:4.16.1-x86_64 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Real signing (requires KMS credentials) | ||||||||||||||||||||||||||||||||
| KMS_CRED_FILE=/path/to/creds KMS_KEY_ID=key-id REKOR_URL=https://... \ | ||||||||||||||||||||||||||||||||
| uv run pyartcd/hack/sign_existing_releases.py \ | ||||||||||||||||||||||||||||||||
|
|
@@ -32,10 +49,10 @@ | |||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||
| from typing import List, Optional | ||||||||||||||||||||||||||||||||
| from typing import Dict, List, Optional, Set | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import click | ||||||||||||||||||||||||||||||||
| from pyartcd.signatory import SigstoreSignatory | ||||||||||||||||||||||||||||||||
| from pyartcd.signatory import ReleaseImageInfo, SigstoreSignatory | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Configure logging | ||||||||||||||||||||||||||||||||
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') | ||||||||||||||||||||||||||||||||
|
|
@@ -66,80 +83,29 @@ def extract_canonical_tag(pullspec: str) -> Optional[str]: | |||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| async def sign_release_pullspec( | ||||||||||||||||||||||||||||||||
| signatory: SigstoreSignatory, | ||||||||||||||||||||||||||||||||
| pullspec: str, | ||||||||||||||||||||||||||||||||
| tag_only: bool = True, | ||||||||||||||||||||||||||||||||
| ) -> bool: | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| Sign a single release pullspec (manifest list or single manifest). | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| For manifest lists, discovers all arch-specific manifests and signs each | ||||||||||||||||||||||||||||||||
| with the manifest list's canonical tag. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| :param signatory: The SigstoreSignatory instance to use | ||||||||||||||||||||||||||||||||
| :param pullspec: The release image pullspec (tag-based preferred) | ||||||||||||||||||||||||||||||||
| :param tag_only: If True, only sign with tag identity (skip digest identity). | ||||||||||||||||||||||||||||||||
| Default is True for retroactive signing where digest signatures already exist. | ||||||||||||||||||||||||||||||||
| :return: True if successful, False if any errors occurred | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| # Extract canonical tag | ||||||||||||||||||||||||||||||||
| canonical_tag = extract_canonical_tag(pullspec) | ||||||||||||||||||||||||||||||||
| if not canonical_tag: | ||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||
| "Cannot determine canonical tag for %s (digest-based pullspec). " | ||||||||||||||||||||||||||||||||
| "Skipping - tag-based pullspecs are required for tag signing.", | ||||||||||||||||||||||||||||||||
| pullspec, | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| return True # Skip but don't count as error | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| logger.info("Processing %s (canonical tag: %s)", pullspec, canonical_tag) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Discover manifests (for manifest lists, gets individual arch manifests) | ||||||||||||||||||||||||||||||||
| # We don't have a release_name to validate against, so we'll skip that check | ||||||||||||||||||||||||||||||||
| release_info, errors = await signatory.discover_release_image( | ||||||||||||||||||||||||||||||||
| pullspec=pullspec, | ||||||||||||||||||||||||||||||||
| canonical_tag=canonical_tag, | ||||||||||||||||||||||||||||||||
| release_name="", # Skip release name validation | ||||||||||||||||||||||||||||||||
| verify_legacy_sig=False, | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if errors: | ||||||||||||||||||||||||||||||||
| for ps, err in errors.items(): | ||||||||||||||||||||||||||||||||
| logger.error("Discovery error for %s: %s", ps, err) | ||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if not release_info.manifests_to_sign: | ||||||||||||||||||||||||||||||||
| logger.warning("No manifests found to sign for %s", pullspec) | ||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| logger.info("Found %d manifest(s) to sign for %s", len(release_info.manifests_to_sign), pullspec) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Sign the release image(s) | ||||||||||||||||||||||||||||||||
| errors = await signatory.sign_release_images([release_info], tag_only=tag_only) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if errors: | ||||||||||||||||||||||||||||||||
| for ps, err in errors.items(): | ||||||||||||||||||||||||||||||||
| logger.error("Signing error for %s: %s", ps, err) | ||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| logger.info("Successfully signed %s", pullspec) | ||||||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| async def main_async( | ||||||||||||||||||||||||||||||||
| pullspecs: List[str], | ||||||||||||||||||||||||||||||||
| dry_run: bool, | ||||||||||||||||||||||||||||||||
| concurrency: int, | ||||||||||||||||||||||||||||||||
| sign_digest: bool = False, | ||||||||||||||||||||||||||||||||
| sign_release: str = "yes", | ||||||||||||||||||||||||||||||||
| ) -> int: | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| Main async entry point for signing release images. | ||||||||||||||||||||||||||||||||
| Main async entry point for signing release images and/or referenced components. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Mirrors the phased flow of the `sigstore-sign` pipeline: | ||||||||||||||||||||||||||||||||
| 1. Discover release images and their manifests (if signing release images). | ||||||||||||||||||||||||||||||||
| 2. Discover referenced component images by spidering each payload (if signing components). | ||||||||||||||||||||||||||||||||
| 3. Sign release image manifests (with canonical tags). | ||||||||||||||||||||||||||||||||
| 4. Sign component images (digest identity only). | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| :param pullspecs: List of pullspecs to sign | ||||||||||||||||||||||||||||||||
| :param pullspecs: List of release pullspecs to process | ||||||||||||||||||||||||||||||||
| :param dry_run: If True, don't actually sign anything | ||||||||||||||||||||||||||||||||
| :param concurrency: Maximum concurrent operations | ||||||||||||||||||||||||||||||||
| :param sign_digest: If True, also sign with digest identity (default: False, tag only) | ||||||||||||||||||||||||||||||||
| :param sign_digest: If True, also sign release images with digest identity | ||||||||||||||||||||||||||||||||
| (default: False, tag only). Does not affect component images. | ||||||||||||||||||||||||||||||||
| :param sign_release: One of "yes" (release images + components), "only" (release | ||||||||||||||||||||||||||||||||
| images only), or "no" (referenced component images only). | ||||||||||||||||||||||||||||||||
| :return: Exit code (0 for success, 1 for errors) | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| # Validate environment | ||||||||||||||||||||||||||||||||
|
|
@@ -166,37 +132,96 @@ async def main_async( | |||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| tag_only = not sign_digest | ||||||||||||||||||||||||||||||||
| logger.info("Starting to sign %d release image(s)...", len(pullspecs)) | ||||||||||||||||||||||||||||||||
| logger.info("Mode: %s", "TAG ONLY (skipping digest signatures)" if tag_only else "BOTH digest and tag signatures") | ||||||||||||||||||||||||||||||||
| if dry_run: | ||||||||||||||||||||||||||||||||
| logger.info("[DRY RUN MODE] No actual signing will occur") | ||||||||||||||||||||||||||||||||
| do_sign_release = sign_release != "no" | ||||||||||||||||||||||||||||||||
| do_sign_components = sign_release != "only" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Process each pullspec | ||||||||||||||||||||||||||||||||
| success_count = 0 | ||||||||||||||||||||||||||||||||
| error_count = 0 | ||||||||||||||||||||||||||||||||
| # Clean input: drop blanks and comment lines | ||||||||||||||||||||||||||||||||
| cleaned = [ps.strip() for ps in pullspecs if ps.strip() and not ps.strip().startswith("#")] | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| for i, pullspec in enumerate(pullspecs, 1): | ||||||||||||||||||||||||||||||||
| pullspec = pullspec.strip() | ||||||||||||||||||||||||||||||||
| if not pullspec or pullspec.startswith("#"): | ||||||||||||||||||||||||||||||||
| continue # Skip empty lines and comments | ||||||||||||||||||||||||||||||||
| logger.info("Starting to process %d release pullspec(s)...", len(cleaned)) | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Fail when input cleaning removes every pullspec. If every supplied value is blank or a comment, Proposed fix cleaned = [ps.strip() for ps in pullspecs if ps.strip() and not ps.strip().startswith("#")]
+ if not cleaned:
+ logger.error("No valid pullspecs provided after removing blank lines and comments.")
+ return 1
logger.info("Starting to process %d release pullspec(s)...", len(cleaned))📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||
| "Release images: %s | Component images: %s", | ||||||||||||||||||||||||||||||||
| ("TAG ONLY" if tag_only else "digest+tag") if do_sign_release else "SKIP", | ||||||||||||||||||||||||||||||||
| "digest only" if do_sign_components else "SKIP", | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| if dry_run: | ||||||||||||||||||||||||||||||||
| logger.info("[DRY RUN MODE] No actual signing will occur") | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| logger.info("--- [%d/%d] Processing %s ---", i, len(pullspecs), pullspec) | ||||||||||||||||||||||||||||||||
| all_errors: Dict[str, Exception] = {} | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # --- Phase 1: Discover release images and their manifests --- | ||||||||||||||||||||||||||||||||
| release_images: List[ReleaseImageInfo] = [] | ||||||||||||||||||||||||||||||||
| if do_sign_release: | ||||||||||||||||||||||||||||||||
| for pullspec in cleaned: | ||||||||||||||||||||||||||||||||
| canonical_tag = extract_canonical_tag(pullspec) | ||||||||||||||||||||||||||||||||
| if not canonical_tag: | ||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||
| "Cannot determine canonical tag for %s (digest-based pullspec). " | ||||||||||||||||||||||||||||||||
| "Skipping release-image signing for it (tag-based pullspecs required).", | ||||||||||||||||||||||||||||||||
| pullspec, | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||
| logger.info("Discovering release image %s (canonical tag: %s)", pullspec, canonical_tag) | ||||||||||||||||||||||||||||||||
| release_info, errors = await signatory.discover_release_image( | ||||||||||||||||||||||||||||||||
| pullspec=pullspec, | ||||||||||||||||||||||||||||||||
| canonical_tag=canonical_tag, | ||||||||||||||||||||||||||||||||
| release_name="", # Skip release name validation | ||||||||||||||||||||||||||||||||
| verify_legacy_sig=False, | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| release_images.append(release_info) | ||||||||||||||||||||||||||||||||
| all_errors.update(errors) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # --- Phase 2: Discover referenced component images from each payload --- | ||||||||||||||||||||||||||||||||
| component_images: Set[str] = set() | ||||||||||||||||||||||||||||||||
| if do_sign_components: | ||||||||||||||||||||||||||||||||
| for pullspec in cleaned: | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This all-discovery-first approach surprises me, although it can clearly work. It might be easier to think about if we keep
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||
| canonical_tag = extract_canonical_tag(pullspec) | ||||||||||||||||||||||||||||||||
| if canonical_tag and canonical_tag.endswith("-multi"): | ||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||
| "%s is a multi payload; `oc adm release info` returns only one arch's " | ||||||||||||||||||||||||||||||||
| "references. To sign all referenced images across every architecture, pass " | ||||||||||||||||||||||||||||||||
| "the per-arch release pullspecs instead of (or in addition to) the -multi one.", | ||||||||||||||||||||||||||||||||
| pullspec, | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| logger.info("Discovering component images from %s", pullspec) | ||||||||||||||||||||||||||||||||
| components, errors = await signatory.discover_component_images( | ||||||||||||||||||||||||||||||||
| release_pullspec=pullspec, | ||||||||||||||||||||||||||||||||
| release_name="", # Not used for component discovery | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| component_images.update(components) | ||||||||||||||||||||||||||||||||
| all_errors.update(errors) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if all_errors: | ||||||||||||||||||||||||||||||||
| for ps, err in all_errors.items(): | ||||||||||||||||||||||||||||||||
| logger.error("Discovery error for %s: %s", ps, err) | ||||||||||||||||||||||||||||||||
| return 1 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # --- Phase 3: Sign release images (with canonical tags) --- | ||||||||||||||||||||||||||||||||
| if release_images: | ||||||||||||||||||||||||||||||||
| total_manifests = sum(len(ri.manifests_to_sign) for ri in release_images) | ||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||
| "Signing %d release image(s) with %d total manifest(s) [%s]", | ||||||||||||||||||||||||||||||||
| len(release_images), | ||||||||||||||||||||||||||||||||
| total_manifests, | ||||||||||||||||||||||||||||||||
| "TAG ONLY" if tag_only else "digest+tag", | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| if errors := await signatory.sign_release_images(release_images, tag_only=tag_only): | ||||||||||||||||||||||||||||||||
| for ps, err in errors.items(): | ||||||||||||||||||||||||||||||||
| logger.error("Release image signing error for %s: %s", ps, err) | ||||||||||||||||||||||||||||||||
| return 1 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||
| success = await sign_release_pullspec(signatory, pullspec, tag_only=tag_only) | ||||||||||||||||||||||||||||||||
| if success: | ||||||||||||||||||||||||||||||||
| success_count += 1 | ||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||
| error_count += 1 | ||||||||||||||||||||||||||||||||
| except Exception as exc: | ||||||||||||||||||||||||||||||||
| logger.exception("Unexpected error processing %s: %s", pullspec, exc) | ||||||||||||||||||||||||||||||||
| error_count += 1 | ||||||||||||||||||||||||||||||||
| # --- Phase 4: Sign component images (digest identity only) --- | ||||||||||||||||||||||||||||||||
| if component_images: | ||||||||||||||||||||||||||||||||
| logger.info("Signing %d component image(s) [digest only]", len(component_images)) | ||||||||||||||||||||||||||||||||
| if errors := await signatory.sign_component_images(component_images): | ||||||||||||||||||||||||||||||||
| for ps, err in errors.items(): | ||||||||||||||||||||||||||||||||
| logger.error("Component image signing error for %s: %s", ps, err) | ||||||||||||||||||||||||||||||||
| return 1 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Summary | ||||||||||||||||||||||||||||||||
| logger.info("=" * 60) | ||||||||||||||||||||||||||||||||
| logger.info("Signing complete: %d successful, %d errors", success_count, error_count) | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why drop the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that was an overreach, restored now |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return 0 if error_count == 0 else 1 | ||||||||||||||||||||||||||||||||
| logger.info("Signing complete!") | ||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @click.command() | ||||||||||||||||||||||||||||||||
|
|
@@ -224,41 +249,62 @@ async def main_async( | |||||||||||||||||||||||||||||||
| "--sign-digest", | ||||||||||||||||||||||||||||||||
| is_flag=True, | ||||||||||||||||||||||||||||||||
| default=False, | ||||||||||||||||||||||||||||||||
| help="Also sign with digest identity (default: tag-only for retroactive signing)", | ||||||||||||||||||||||||||||||||
| help="Also sign release images with digest identity (default: tag-only for retroactive signing)", | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| @click.option( | ||||||||||||||||||||||||||||||||
| "--sign-release", | ||||||||||||||||||||||||||||||||
| type=click.Choice(("yes", "no", "only")), | ||||||||||||||||||||||||||||||||
| default="yes", | ||||||||||||||||||||||||||||||||
| help=( | ||||||||||||||||||||||||||||||||
| "What to sign: 'yes' = release images + referenced components (default), " | ||||||||||||||||||||||||||||||||
| "'only' = release images only, 'no' = referenced components only." | ||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| @click.argument("pullspecs", nargs=-1) | ||||||||||||||||||||||||||||||||
| def main( | ||||||||||||||||||||||||||||||||
| dry_run: bool, | ||||||||||||||||||||||||||||||||
| input_file: Optional[str], | ||||||||||||||||||||||||||||||||
| concurrency: int, | ||||||||||||||||||||||||||||||||
| sign_digest: bool, | ||||||||||||||||||||||||||||||||
| sign_release: str, | ||||||||||||||||||||||||||||||||
| pullspecs: tuple, | ||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| Sign existing release images with Sigstore/cosign. | ||||||||||||||||||||||||||||||||
| Sign existing release images (and optionally their referenced components) with Sigstore/cosign. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| PULLSPECS are tag-based release image pullspecs like: | ||||||||||||||||||||||||||||||||
| quay.io/openshift-release-dev/ocp-release:4.16.1-multi | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| For manifest lists, all arch-specific manifests will be discovered and signed. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| By default, only TAG-BASED signatures are created (digest signatures are skipped). | ||||||||||||||||||||||||||||||||
| This is appropriate for retroactive signing where digest signatures already exist. | ||||||||||||||||||||||||||||||||
| Use --sign-digest to also create digest-based signatures. | ||||||||||||||||||||||||||||||||
| Release images are signed with TAG-BASED signatures only by default (digest | ||||||||||||||||||||||||||||||||
| signatures are skipped, appropriate for retroactive signing where digest | ||||||||||||||||||||||||||||||||
| signatures already exist). Use --sign-digest to also create digest signatures. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Referenced component images are discovered by spidering each payload with | ||||||||||||||||||||||||||||||||
| `oc adm release info -o json` and are always signed with digest identity only. | ||||||||||||||||||||||||||||||||
| Use --sign-release to choose whether to sign release images, components, or both. | ||||||||||||||||||||||||||||||||
| NOTE: a `-multi` payload only yields one arch's references, so pass the per-arch | ||||||||||||||||||||||||||||||||
| release pullspecs to cover all referenced images across every architecture. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Examples: | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| \b | ||||||||||||||||||||||||||||||||
| # Dry run with a single pullspec (tag-only signing) | ||||||||||||||||||||||||||||||||
| # Dry run: sign a release image and its referenced components | ||||||||||||||||||||||||||||||||
| uv run pyartcd/hack/sign_existing_releases.py --dry-run \\ | ||||||||||||||||||||||||||||||||
| quay.io/openshift-release-dev/ocp-release:4.16.1-multi | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| \b | ||||||||||||||||||||||||||||||||
| # Sign multiple from a file | ||||||||||||||||||||||||||||||||
| uv run pyartcd/hack/sign_existing_releases.py --dry-run -f pullspecs.txt | ||||||||||||||||||||||||||||||||
| # Sign only the release images from a file | ||||||||||||||||||||||||||||||||
| uv run pyartcd/hack/sign_existing_releases.py --dry-run --sign-release only -f pullspecs.txt | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| \b | ||||||||||||||||||||||||||||||||
| # Sign only the referenced component images | ||||||||||||||||||||||||||||||||
| uv run pyartcd/hack/sign_existing_releases.py --dry-run --sign-release no \\ | ||||||||||||||||||||||||||||||||
| quay.io/openshift-release-dev/ocp-release:4.16.1-x86_64 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| \b | ||||||||||||||||||||||||||||||||
| # Also sign with digest identity | ||||||||||||||||||||||||||||||||
| # Also sign release images with digest identity | ||||||||||||||||||||||||||||||||
| uv run pyartcd/hack/sign_existing_releases.py --dry-run --sign-digest \\ | ||||||||||||||||||||||||||||||||
| quay.io/openshift-release-dev/ocp-release:4.16.1-x86_64 | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
|
|
@@ -276,7 +322,7 @@ def main( | |||||||||||||||||||||||||||||||
| sys.exit(1) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Run async main | ||||||||||||||||||||||||||||||||
| exit_code = asyncio.run(main_async(all_pullspecs, dry_run, concurrency, sign_digest)) | ||||||||||||||||||||||||||||||||
| exit_code = asyncio.run(main_async(all_pullspecs, dry_run, concurrency, sign_digest, sign_release)) | ||||||||||||||||||||||||||||||||
| sys.exit(exit_code) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems workable, although personally
--sign-release noseems a bit awkward as a way to say "sign only the referenced component images. Maybe pivot to--sign (release|release-image|component-images)or some such that avoids going boolean-ish? Or just leave it as you have it, because I expect folks to run this command very rarely, so there's not much value in polishing its interface.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree it is a bit awkward. This being the existing choice in sigstore pipeline makes it a familiar pattern -
art-tools/pyartcd/pyartcd/pipelines/sigstore_sign.py
Line 218 in ea7f4ae