Skip to content

Commit

Permalink
Add new task to request signature of blobs
Browse files Browse the repository at this point in the history
- Add request-signature-blob task to be able of using request-signature
  to sign a blob
- Modify request-signature.py to sign container images and blobs
  depending of the arguments passed

Signed-off-by: Ernesto González <[email protected]>
  • Loading branch information
ernesgonzalez33 authored and Allda committed Dec 1, 2023
1 parent 050568c commit 18d3e21
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: request-signature-blob
spec:
params:
- name: pipeline_image
description: A docker image of operator-pipeline-images for the steps to run in.
- name: blob
description: Blob that needs to be signed.
- name: requester
description: Name of the user that requested the signing, for auditing purposes
- name: sig_key_id
description: The signing key id that the content is signed with
default: "4096R/55A34A82 SHA-256"
- name: sig_key_name
description: The signing key name that the content is signed with
default: containerisvsign
- name: umb_ssl_secret_name
description: Kubernetes secret name that contains the umb SSL files
- name: umb_ssl_cert_secret_key
description: The key within the Kubernetes secret that contains the umb SSL cert.
- name: umb_ssl_key_secret_key
description: The key within the Kubernetes secret that contains the umb SSL key.
- name: umb_client_name
description: Client name to connect to umb, usually a service account name
default: operatorpipelines
- name: umb_listen_topic
description: umb topic to listen to for responses with signed content
default: VirtualTopic.eng.robosignatory.isv.sign
- name: umb_publish_topic
description: umb topic to publish to for requesting signing
default: VirtualTopic.eng.operatorpipelines.isv.sign
- name: umb_url
description: umb host to connect to for messaging
default: umb.api.redhat.com
results:
- name: signed_payload
volumes:
- name: umb-ssl-volume
secret:
secretName: "$(params.umb_ssl_secret_name)"
optional: false
workspaces:
- name: source
steps:
- name: request-signature-blob
image: "$(params.pipeline_image)"
env:
- name: UMB_CERT_PATH
value: /etc/umb-ssl-volume/$(params.umb_ssl_cert_secret_key)
- name: UMB_KEY_PATH
value: /etc/umb-ssl-volume/$(params.umb_ssl_key_secret_key)
volumeMounts:
- name: umb-ssl-volume
readOnly: true
mountPath: "/etc/umb-ssl-volume"
script: |
#! /usr/bin/env bash
set -xe
echo "Requesting signing from RADAS"
request-signature \
--blob "$(params.blob)" \
--output signing_response.json \
--requester "$(params.requester)" \
--sig-key-id "$(params.sig_key_id)" \
--sig-key-name "$(params.sig_key_name)" \
--umb-client-name "$(params.umb_client_name)" \
--umb-listen-topic "$(params.umb_listen_topic)" \
--umb-publish-topic "$(params.umb_publish_topic)" \
--umb-url "$(params.umb_url)" \
--verbose
SIG_DATA=$(cat signing_response.json)
echo "Signed claims and their metadata: "
echo -n $SIG_DATA
jq -r '.[0].signed_payload' signing_response.json | tee $(results.signed_payload.path)
workingDir: $(workspaces.source.path)
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ def setup_argparser() -> Any: # pragma: no cover
parser = argparse.ArgumentParser(
description="Cli tool to request signature from RADAS"
)
parser.add_argument(
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--manifest-digest",
help="Manifest digest for the signed content, usually in the format sha256:xxx,"
"separated by commas if there are multiple",
required=True,
)
parser.add_argument(
"--output",
Expand All @@ -44,7 +44,11 @@ def setup_argparser() -> Any: # pragma: no cover
help="Docker reference for the signed content, "
"e.g. registry.redhat.io/redhat/community-operator-index:v4.9,"
"separated by commas if there are multiple",
required=True,
)
group.add_argument(
"--blob",
help="Blob that needs to be signed. Encoded in base64 format. "
"Separated by commas if there are multiple",
)
parser.add_argument(
"--requester",
Expand Down Expand Up @@ -227,23 +231,61 @@ def gen_request_msg(
return request_msg


def gen_request_msg_blob(args: Any, blob: str, request_id: str) -> Dict[str, Any]:
"""
Generate the request message to send to RADAS.
Args:
args: Args from script input.
blob: Blob that needs to be signed.
request_id: UUID to identify match the request with RADAS's response.
Returns:
"""
request_msg = {
"artifact": blob,
"request_id": request_id,
"requested_by": args.requester,
"sig_keyname": args.sig_key_name,
"sig_key_id": args.sig_key_id,
}
return request_msg


def request_signature( # pylint: disable=too-many-branches,too-many-statements,too-many-locals
args: Any,
) -> None:
"""
Format and send out a UMB message to request signing, and retry as needed.
"""
manifests = args.manifest_digest.strip(",").split(",")
references = args.reference.strip(",").split(",")

if len(manifests) != len(references):
output_file = args.output
manifests = []
references = []
blobs = []

# Fill the arrays for manifests and references, or blobs
if args.manifest_digest is not None and args.reference is not None:
manifests = args.manifest_digest.strip(",").split(",")
references = args.reference.strip(",").split(",")

if len(manifests) != len(references):
LOGGER.error(
"Manifest digest list does not match the length of reference list."
)
sys.exit(1)
elif args.blob is not None:
if args.reference is not None:
LOGGER.warning(
"When signing blobs, reference is not needed. It will be ignored."
)
blobs = args.blob.strip(",").split(",")
else:
LOGGER.error(
"Manifest digest list does not match the length of reference list."
"--reference is needed when --manifest-digest is used to sign images"
)
sys.exit(1)

output_file = args.output

umb = start_umb_client(
hosts=[args.umb_url],
client_name=args.umb_client_name,
Expand All @@ -253,15 +295,26 @@ def request_signature( # pylint: disable=too-many-branches,too-many-statements,
request_msgs = {}
global request_ids # pylint: disable=global-statement
request_ids = set()
for manifest, reference in zip(manifests, references):
request_id = str(uuid.uuid4())
request_msgs[request_id] = gen_request_msg(
args=args,
digest=manifest,
reference=reference,
request_id=request_id,
)
request_ids.add(request_id)

if len(manifests) > 0:
for manifest, reference in zip(manifests, references):
request_id = str(uuid.uuid4())
request_msgs[request_id] = gen_request_msg(
args=args,
digest=manifest,
reference=reference,
request_id=request_id,
)
request_ids.add(request_id)
else:
for blob in blobs:
request_id = str(uuid.uuid4())
request_msgs[request_id] = gen_request_msg_blob(
args=args,
blob=blob,
request_id=request_id,
)
request_ids.add(request_id)

umb.connect_and_subscribe(args.umb_listen_topic)

Expand Down
117 changes: 117 additions & 0 deletions operator-pipeline-images/tests/entrypoints/test_request_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,42 @@ def test_gen_request_msg(
}


def test_gen_request_msg_blob() -> None:
args = MagicMock()
args.output = "output.json"
args.requester = "test-requester"
args.sig_key_name = "testkey"
args.sig_key_id = "123"

request_msg_blob = request_signature.gen_request_msg_blob(
args,
blob="test-blob",
request_id="request_id123",
)
assert request_msg_blob == {
"artifact": "test-blob",
"request_id": "request_id123",
"requested_by": args.requester,
"sig_keyname": args.sig_key_name,
"sig_key_id": args.sig_key_id,
}


def test_request_signature_uneven_manifest_and_reference() -> None:
args = MagicMock
args.manifest_digest = "a,b,c"
args.reference = "d,e"
args.output = "signing_response.json"
with pytest.raises(SystemExit) as e:
request_signature.request_signature(args)


def test_request_signature_manifest_and_blob() -> None:
args = MagicMock
args.manifest_digest = "manifest"
args.reference = None
args.blob = None
args.output = "signing_response.json"
with pytest.raises(SystemExit) as e:
request_signature.request_signature(args)

Expand Down Expand Up @@ -194,6 +226,91 @@ def test_request_signature_multi_request_no_retry(
mock_umb.stop.assert_called_once()


@patch("json.load")
@patch("time.sleep")
@patch("os.path.exists")
@patch("operatorcert.entrypoints.request_signature.gen_request_msg_blob")
@patch("operatorcert.entrypoints.request_signature.start_umb_client")
def test_request_signature_single_request_no_retry_blob(
mock_start_umb: MagicMock,
mock_request_msg: MagicMock,
mock_path_exists: MagicMock,
mock_sleep: MagicMock,
mock_json_load: MagicMock,
) -> None:
mock_umb = MagicMock()
mock_start_umb.return_value = mock_umb
mock_path_exists.return_value = True
mock_request_msg.return_value = {"request_id": "request_id123"}
mock_json_load.return_value = {
"request_id": "request_id123",
"signing_status": "success",
}
args = MagicMock()
args.blob = "test-blob"
args.manifest_digest = None
args.reference = None
args.output = "output.json"
args.umb_client_name = "test-client"
args.umb_url = "test.umb"
args.umb_listen_topic = "Virtualtopic.test.listen"
args.umb_publish_topic = "Virtualtopic.test.publish"

mock_open = mock.mock_open()
with mock.patch("builtins.open", mock_open):
request_signature.request_signature(args)
mock_umb.connect_and_subscribe.assert_called_once_with(args.umb_listen_topic)
mock_umb.send.assert_called_once_with(
args.umb_publish_topic, json.dumps({"request_id": "request_id123"})
)
mock_sleep.assert_called_once()
mock_umb.unsubscribe.assert_called_once_with(args.umb_listen_topic)
mock_umb.stop.assert_called_once()


@patch("json.load")
@patch("time.sleep")
@patch("os.path.exists")
@patch("operatorcert.entrypoints.request_signature.gen_request_msg_blob")
@patch("operatorcert.entrypoints.request_signature.start_umb_client")
def test_request_signature_multi_request_no_retry_blob_with_ignored_reference(
mock_start_umb: MagicMock,
mock_request_msg: MagicMock,
mock_path_exists: MagicMock,
mock_sleep: MagicMock,
mock_json_load: MagicMock,
) -> None:
mock_umb = MagicMock()
mock_start_umb.return_value = mock_umb
mock_path_exists.return_value = True
mock_request_msg.return_value = {"request_id": "request_id123"}
mock_json_load.return_value = {
"request_id": "request_id123",
"signing_status": "success",
}
args = MagicMock()
args.blob = "test-blob1,test-blob2,test-blob3"
args.output = "output.json"
args.manifest_digest = None
args.reference = "test-reference"
args.umb_client_name = "test-client"
args.umb_url = "test.umb"
args.umb_listen_topic = "Virtualtopic.test.listen"
args.umb_publish_topic = "Virtualtopic.test.publish"

mock_open = mock.mock_open()
with mock.patch("builtins.open", mock_open):
request_signature.request_signature(args)
mock_umb.connect_and_subscribe.assert_called_once_with(args.umb_listen_topic)
mock_umb.send.assert_called_with(
args.umb_publish_topic, json.dumps({"request_id": "request_id123"})
)
assert mock_umb.send.call_count == 3
mock_sleep.assert_called_once()
mock_umb.unsubscribe.assert_called_once_with(args.umb_listen_topic)
mock_umb.stop.assert_called_once()


@patch("json.load")
@patch("sys.exit")
@patch("time.sleep")
Expand Down

0 comments on commit 18d3e21

Please sign in to comment.