Skip to content

Commit 169f685

Browse files
authored
Add task and script to upload signature to Pyxis sigstore (#229)
Add task and script to upload signature to Pyxis sigstore
1 parent c76d5ea commit 169f685

File tree

5 files changed

+246
-0
lines changed

5 files changed

+246
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
apiVersion: tekton.dev/v1beta1
3+
kind: Task
4+
metadata:
5+
name: upload-signature
6+
spec:
7+
params:
8+
- name: docker_reference
9+
description: Docker reference for the signed content, e.g. registry.redhat.io/redhat/community-operator-index:v4.9
10+
- name: manifest_digest
11+
description: Manifest digest for the signed content, usually in the format sha256:xxx
12+
- name: repository
13+
description: Name of the repository that hosts the signed content, e.g. redhat/community-operator-index
14+
- name: sig_key_id
15+
description: The signing key id that the content was signed with.
16+
- name: signature_data
17+
description: The signature to upload to Pyxis signature.
18+
- name: pipeline_image
19+
description: A docker image of operator-pipeline-images for the steps to run in.
20+
- name: sig_key_pub_key
21+
description: Env var that points to the location of the signing key's public key,
22+
to be imported to gpg for verification.
23+
default: ISVSIGN_PUB_KEY
24+
- name: pyxis_ssl_secret_name
25+
description: Kubernetes secret name that contains the Pyxis SSL files.
26+
- name: pyxis_ssl_cert_secret_key
27+
description: The key within the Kubernetes secret that contains the Pyxis SSL cert.
28+
- name: pyxis_ssl_key_secret_key
29+
description: The key within the Kubernetes secret that contains the Pyxis SSL key.
30+
- name: pyxis_url
31+
description: Pyxis instance to upload the signature to.
32+
default: https://pyxis.engineering.redhat.com
33+
- name: verify_signature
34+
description: Whether to verify that the signature data is signed with the right key.
35+
default: "true"
36+
volumes:
37+
- name: pyxis-ssl-volume
38+
secret:
39+
secretName: "$(params.pyxis_ssl_secret_name)"
40+
optional: false
41+
steps:
42+
- name: verify-signature
43+
image: "$(params.pipeline_image)"
44+
script: |
45+
#! /usr/bin/env bash
46+
set -xe
47+
48+
if [[ "$(params.verify_signature)" == "true" ]]; then
49+
echo "Verifying signature before upload"
50+
echo $(params.signature_data) | base64 --decode > decoded_signed_claim
51+
52+
gpg --import $(printenv $(params.sig_key_pub_key))
53+
gpg --verify decoded_signed_claim
54+
fi
55+
56+
- name: upload-signature
57+
image: "$(params.pipeline_image)"
58+
env:
59+
- name: PYXIS_CERT_PATH
60+
value: /etc/pyxis-ssl-volume/$(params.pyxis_ssl_cert_secret_key)
61+
- name: PYXIS_KEY_PATH
62+
value: /etc/pyxis-ssl-volume/$(params.pyxis_ssl_key_secret_key)
63+
volumeMounts:
64+
- name: pyxis-ssl-volume
65+
readOnly: true
66+
mountPath: "/etc/pyxis-ssl-volume"
67+
script: |
68+
#! /usr/bin/env bash
69+
set -xe
70+
71+
echo "Signature verified. Uploading to Pyxis sigstore"
72+
upload-signature \
73+
--pyxis-url "$(params.pyxis_url)" \
74+
--manifest-digest "$(params.manifest_digest)" \
75+
--reference "$(params.docker_reference)" \
76+
--repository "$(params.repository)" \
77+
--sig-key-id "$(params.sig_key_id)" \
78+
--signature-data "$(params.signature_data)" \
79+
--verbose

operator-pipeline-images/Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ RUN curl -LO https://github.com/operator-framework/operator-registry/releases/do
4141
chmod +x linux-amd64-opm && \
4242
mv linux-amd64-opm /usr/local/bin/opm
4343

44+
# Download ISV Container Signing Pub Key for signature verification
45+
RUN curl https://www.redhat.com/security/data/55A34A82.txt -o /etc/containerisvsign.txt && \
46+
chmod go=r /etc/containerisvsign.txt
47+
ENV ISVSIGN_PUB_KEY=/etc/containerisvsign.txt
48+
4449
RUN useradd -ms /bin/bash -u "${USER_UID}" user
4550

4651
WORKDIR /home/user
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import argparse
2+
import logging
3+
from typing import Any
4+
from urllib.parse import urljoin
5+
from requests.exceptions import HTTPError
6+
7+
from operatorcert import pyxis
8+
from operatorcert.logger import setup_logger
9+
10+
LOGGER = logging.getLogger("operator-cert")
11+
12+
13+
def setup_argparser() -> Any: # pragma: no cover
14+
"""
15+
Setup argument parser
16+
17+
Returns:
18+
Any: Initialized argument parser
19+
"""
20+
parser = argparse.ArgumentParser(
21+
description="Cli tool to upload signature to Pyxis"
22+
)
23+
24+
parser.add_argument(
25+
"--pyxis-url",
26+
default="https://pyxis.engineering.redhat.com",
27+
help="Base URL for Pyxis container metadata API",
28+
)
29+
parser.add_argument(
30+
"--manifest-digest",
31+
help="Manifest digest for the signed content, usually in the format sha256:xxx",
32+
required=True,
33+
)
34+
parser.add_argument(
35+
"--reference",
36+
help="Docker reference for the signed content, e.g. registry.redhat.io/redhat/community-operator-index:v4.9",
37+
required=True,
38+
)
39+
parser.add_argument(
40+
"--repository",
41+
help="Name of the repository that hosts the signed content, e.g. redhat/community-operator-index",
42+
required=True,
43+
)
44+
parser.add_argument(
45+
"--sig-key-id",
46+
help="The signing key id that the content was signed with",
47+
required=True,
48+
)
49+
parser.add_argument("--signature-data", help="The signed content", required=True)
50+
parser.add_argument("--verbose", action="store_true", help="Verbose output")
51+
return parser
52+
53+
54+
def upload_signature(args: Any) -> None:
55+
"""
56+
Upload signature to Pyxis
57+
58+
Args:
59+
args (Any): CLI arguments
60+
61+
Returns:
62+
Dict[str, Any]]: Pyxis respones
63+
"""
64+
payload = {
65+
"manifest_digest": args.manifest_digest,
66+
"reference": args.reference,
67+
"repository": args.repository,
68+
"sig_key_id": args.sig_key_id,
69+
"signature_data": args.signature_data,
70+
}
71+
72+
try:
73+
rsp_json = pyxis.post(urljoin(args.pyxis_url, "v1/signatures"), payload)
74+
LOGGER.info("Signature successfully created on Pyxis: %s", rsp_json)
75+
except HTTPError as err:
76+
if err.response.status_code == 409:
77+
LOGGER.warning(
78+
"Pyxis POST request resulted in 409 Conflict Error. Signature may have "
79+
"already been uploaded previously."
80+
)
81+
else:
82+
raise
83+
84+
85+
def main(): # pragma: no cover
86+
"""
87+
Main func
88+
"""
89+
90+
parser = setup_argparser()
91+
args = parser.parse_args()
92+
log_level = "DEBUG" if args.verbose else "INFO"
93+
setup_logger(log_level)
94+
95+
upload_signature(args)
96+
97+
98+
if __name__ == "__main__": # pragma: no cover
99+
main()

operator-pipeline-images/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"create-container-image=operatorcert.entrypoints.create_container_image:main",
4343
"marketplace-replication=operatorcert.entrypoints.marketplace_replication:main",
4444
"pipelinerun-summary=operatorcert.entrypoints.pipelinerun_summary:main",
45+
"upload-signature=operatorcert.entrypoints.upload_signature:main",
4546
],
4647
},
4748
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import pytest
2+
from requests.exceptions import HTTPError
3+
from requests.models import Response
4+
from unittest.mock import MagicMock, patch
5+
6+
from operatorcert.entrypoints import upload_signature
7+
8+
9+
@patch("operatorcert.entrypoints.upload_signature.setup_argparser")
10+
@patch("operatorcert.entrypoints.upload_signature.upload_signature")
11+
def test_main(
12+
mock_upload_signature: MagicMock,
13+
mock_arg_parser: MagicMock,
14+
) -> None:
15+
upload_signature.main()
16+
mock_upload_signature.assert_called_once()
17+
18+
19+
@patch("operatorcert.entrypoints.upload_signature.pyxis.post")
20+
def test_upload_signature(mock_post: MagicMock) -> None:
21+
args = MagicMock()
22+
args.pyxis_url = "https://test-pyxis.fake.url"
23+
args.manifest_digest = "sha256:123456"
24+
args.reference = "registry.redhat.io/redhat/community-operator-index:v4.9"
25+
args.repository = "redhat/community-operator-index"
26+
args.sig_key_id = "testkeyid"
27+
args.signature_data = "dGVzdHNpZ25hdHVyZWRhdGEK"
28+
29+
upload_signature.upload_signature(args)
30+
mock_post.assert_called_once_with(
31+
"https://test-pyxis.fake.url/v1/signatures",
32+
{
33+
"manifest_digest": args.manifest_digest,
34+
"reference": args.reference,
35+
"repository": args.repository,
36+
"sig_key_id": args.sig_key_id,
37+
"signature_data": args.signature_data,
38+
},
39+
)
40+
41+
42+
@patch("operatorcert.entrypoints.upload_signature.pyxis.post")
43+
def test_upload_signature_http_errors(mock_post: MagicMock) -> None:
44+
args = MagicMock()
45+
args.pyxis_url = "https://test-pyxis.fake.url"
46+
args.manifest_digest = "sha256:123456"
47+
args.reference = "registry.redhat.io/redhat/community-operator-index:v4.9"
48+
args.repository = "redhat/community-operator-index"
49+
args.sig_key_id = "testkeyid"
50+
args.signature_data = "dGVzdHNpZ25hdHVyZWRhdGEK"
51+
52+
fake_rsp = Response()
53+
fake_rsp.status_code = 409
54+
fake_err = HTTPError(response=fake_rsp)
55+
mock_post.side_effect = fake_err
56+
upload_signature.upload_signature(args)
57+
58+
fake_rsp.status_code = 404
59+
fake_err = HTTPError(response=fake_rsp)
60+
mock_post.side_effect = fake_err
61+
with pytest.raises(HTTPError):
62+
upload_signature.upload_signature(args)

0 commit comments

Comments
 (0)