Skip to content

Commit 245defa

Browse files
authored
Add task to request signing of an index image from RADAS (#258)
Add task to request signing of an index image from RADAS
1 parent ed0e172 commit 245defa

File tree

9 files changed

+782
-1
lines changed

9 files changed

+782
-1
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: request-signature
6+
spec:
7+
params:
8+
- name: pipeline_image
9+
description: A docker image of operator-pipeline-images for the steps to run in.
10+
- name: manifest_digest
11+
description: Manifest digest for the signed content, usually in the format sha256:xxx
12+
- name: reference
13+
description: Docker reference for the signed content, e.g. registry.redhat.io/redhat/community-operator-index:v4.9
14+
- name: requester
15+
description: Name of the user that requested the signing, for auditing purposes
16+
- name: sig_key_id
17+
description: The signing key id that the content is signed with
18+
default: "4096R/55A34A82 SHA-256"
19+
- name: sig_key_name
20+
description: The signing key name that the content is signed with
21+
default: containerisvsign
22+
- name: umb_ssl_secret_name
23+
description: Kubernetes secret name that contains the umb SSL files
24+
- name: umb_ssl_cert_secret_key
25+
description: The key within the Kubernetes secret that contains the umb SSL cert.
26+
- name: umb_ssl_key_secret_key
27+
description: The key within the Kubernetes secret that contains the umb SSL key.
28+
- name: umb_client_name
29+
description: Client name to connect to umb, usually a service account name
30+
default: operatorpipelines
31+
- name: umb_listen_topic
32+
description: umb topic to listen to for responses with signed content
33+
default: VirtualTopic.eng.robosignatory.isv.sign
34+
- name: umb_publish_topic
35+
description: umb topic to publish to for requesting signing
36+
default: VirtualTopic.eng.operatorpipelines.isv.sign
37+
- name: umb_url
38+
description: umb host to connect to for messaging
39+
default: umb.api.redhat.com
40+
results:
41+
- name: signed_claim
42+
volumes:
43+
- name: umb-ssl-volume
44+
secret:
45+
secretName: "$(params.umb_ssl_secret_name)"
46+
optional: false
47+
steps:
48+
- name: request-signature
49+
image: "$(params.pipeline_image)"
50+
env:
51+
- name: UMB_CERT_PATH
52+
value: /etc/umb-ssl-volume/$(params.umb_ssl_cert_secret_key)
53+
- name: UMB_KEY_PATH
54+
value: /etc/umb-ssl-volume/$(params.umb_ssl_key_secret_key)
55+
volumeMounts:
56+
- name: umb-ssl-volume
57+
readOnly: true
58+
mountPath: "/etc/umb-ssl-volume"
59+
script: |
60+
#! /usr/bin/env bash
61+
set -xe
62+
63+
echo "Requesting signing from RADAS"
64+
request-signature \
65+
--manifest-digest "$(params.manifest_digest)" \
66+
--output signing_response.json \
67+
--reference "$(params.reference)" \
68+
--requester "$(params.requester)" \
69+
--sig-key-id "$(params.sig_key_id)" \
70+
--sig-key-name "$(params.sig_key_name)" \
71+
--umb-client-name "$(params.umb_client_name)" \
72+
--umb-listen-topic "$(params.umb_listen_topic)" \
73+
--umb-publish-topic "$(params.umb_publish_topic)" \
74+
--umb-url "$(params.umb_url)" \
75+
--verbose
76+
77+
SIGNED_CLAIM=$(cat signing_response.json | jq -r ".signed_claim")
78+
echo "Signed claim: "
79+
echo -n $SIGNED_CLAIM | tee $(results.signed_claim.path)

ansible/roles/operator-pipeline/templates/openshift/tasks/set-env.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ spec:
2020
description: Connect container registry proxy based on selected environment
2121
- name: iib_url
2222
description: IIB URL based on selected environment
23+
- name: sig_key_id
24+
description: The signing key id that index image claims are signed with
25+
- name: sig_key_name
26+
description: The signing key name that index image claims are signed with
27+
- name: umb_url
28+
description: umb host to connect to for messaging, e.g. for signing
29+
- name: umb_client_name
30+
description: Client name to connect to umb, usually a service account name
2331
steps:
2432
- name: set-env
2533
image: registry.access.redhat.com/ubi8-minimal@sha256:54ef2173bba7384dc7609e8affbae1c36f8a3ec137cacc0866116d65dd4b9afe
@@ -49,6 +57,10 @@ spec:
4957
CONNECT_URL="https://connect.redhat.com"
5058
CONNECT_REGISTRY="registry.connect.redhat.com"
5159
IIB_URL="https://iib.engineering.redhat.com"
60+
SIG_KEY_ID="4096R/55A34A82 SHA-256"
61+
SIG_KEY_NAME="containerisvsign"
62+
UMB_URL="umb.api.redhat.com"
63+
UMB_CLIENT_NAME="operatorpipelines"
5264
;;
5365
stage)
5466
case $ACCESS_TYPE in
@@ -63,6 +75,10 @@ spec:
6375
CONNECT_URL="https://connect.stage.redhat.com"
6476
CONNECT_REGISTRY="registry.connect.stage.redhat.com"
6577
IIB_URL="https://iib.stage.engineering.redhat.com"
78+
SIG_KEY_ID="4096R/37036783 SHA-256"
79+
SIG_KEY_NAME="redhate2etesting"
80+
UMB_URL="umb.stage.api.redhat.com"
81+
UMB_CLIENT_NAME="nonprod-operatorpipelines"
6682
;;
6783
qa)
6884
case $ACCESS_TYPE in
@@ -77,6 +93,10 @@ spec:
7793
CONNECT_URL="https://connect.qa.redhat.com"
7894
CONNECT_REGISTRY="registry.connect.qa.redhat.com"
7995
IIB_URL="https://iib.stage.engineering.redhat.com"
96+
SIG_KEY_ID="4096R/37036783 SHA-256"
97+
SIG_KEY_NAME="redhate2etesting"
98+
UMB_URL="umb.stage.api.redhat.com"
99+
UMB_CLIENT_NAME="nonprod-operatorpipelines"
80100
;;
81101
dev)
82102
case $ACCESS_TYPE in
@@ -91,6 +111,10 @@ spec:
91111
CONNECT_URL="https://connect.dev.redhat.com"
92112
CONNECT_REGISTRY="registry.connect.dev.redhat.com"
93113
IIB_URL="https://iib.stage.engineering.redhat.com"
114+
SIG_KEY_ID="4096R/37036783 SHA-256"
115+
SIG_KEY_NAME="redhate2etesting"
116+
UMB_URL="umb.stage.api.redhat.com"
117+
UMB_CLIENT_NAME="nonprod-operatorpipelines"
94118
;;
95119
*)
96120
echo "Unknown environment."
@@ -103,3 +127,7 @@ spec:
103127
echo -n $CONNECT_URL | tee $(results.connect_url.path)
104128
echo -n $CONNECT_REGISTRY | tee $(results.connect_registry.path)
105129
echo -n $IIB_URL | tee $(results.iib_url.path)
130+
echo -n $SIG_KEY_ID | tee $(results.sig_key_id.path)
131+
echo -n $SIG_KEY_NAME | tee $(results.sig_key_name.path)
132+
echo -n $UMB_URL | tee $(results.umb_url.path)
133+
echo -n $UMB_CLIENT_NAME | tee $(results.umb_client_name.path)

operator-pipeline-images/__init__.py

Whitespace-only changes.
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import argparse
2+
import base64
3+
import json
4+
import logging
5+
import os
6+
import sys
7+
import threading
8+
import time
9+
from typing import Any
10+
11+
import stomp
12+
13+
from operatorcert.logger import setup_logger
14+
from operatorcert.umb import start_umb_client
15+
16+
LOGGER = logging.getLogger("operator-cert")
17+
18+
19+
def setup_argparser() -> Any: # pragma: no cover
20+
"""
21+
Setup argument parser
22+
23+
Returns:
24+
Any: Initialized argument parser
25+
"""
26+
parser = argparse.ArgumentParser(
27+
description="Cli tool to request signature from RADAS"
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+
"--output",
36+
help="Path to an output file.",
37+
default="signing_response.json",
38+
)
39+
parser.add_argument(
40+
"--reference",
41+
help="Docker reference for the signed content, e.g. registry.redhat.io/redhat/community-operator-index:v4.9",
42+
required=True,
43+
)
44+
parser.add_argument(
45+
"--requester",
46+
default="[email protected]",
47+
help="Name of the user that requested the signing, for auditing purposes",
48+
required=True,
49+
)
50+
parser.add_argument(
51+
"--sig-key-id",
52+
default="4096R/55A34A82 SHA-256",
53+
help="The signing key id that the content was signed with",
54+
required=True,
55+
)
56+
parser.add_argument(
57+
"--sig-key-name",
58+
default="containerisvsign",
59+
help="The signing key name that the content was signed with",
60+
required=True,
61+
)
62+
parser.add_argument(
63+
"--umb-client-name",
64+
default="operatorpipelines",
65+
help="Client name to connect to umb, usually a service account name",
66+
required=True,
67+
)
68+
parser.add_argument(
69+
"--umb-listen-topic",
70+
default="VirtualTopic.eng.robosignatory.isv.sign",
71+
help="umb topic to listen to for responses with signed content",
72+
required=True,
73+
)
74+
parser.add_argument(
75+
"--umb-publish-topic",
76+
default="VirtualTopic.eng.operatorpipelines.isv.sign",
77+
help="umb topic to publish to for requesting signing",
78+
required=True,
79+
)
80+
parser.add_argument(
81+
"--umb-url",
82+
default="umb.api.redhat.com",
83+
help="umb host to connect to for messaging",
84+
required=True,
85+
)
86+
parser.add_argument("--verbose", action="store_true", help="Verbose output")
87+
return parser
88+
89+
90+
umb = None
91+
result_file = None
92+
93+
# wait for signing response for a total of 5 min, at 5 second intervals
94+
TIMEOUT_COUNT = 60
95+
WAIT_INTERVAL_SEC = 5
96+
97+
98+
class UmbHandler(stomp.ConnectionListener): # pragma: no cover
99+
def on_error(self, frame: Any) -> None:
100+
LOGGER.error("Received an error frame:\n{}".format(frame.body))
101+
102+
def on_message(self, frame: Any) -> None:
103+
# handle response from radas in a thread
104+
t = threading.Thread(target=process_message, args=[frame.body])
105+
t.start()
106+
107+
def on_disconnected(self: Any) -> None:
108+
LOGGER.error("Disconnected from umb.")
109+
110+
111+
def process_message(msg: Any) -> None:
112+
"""
113+
Process a message received from UMB.
114+
Args:
115+
msg: The message body received.
116+
"""
117+
msg = json.loads(msg)["msg"]
118+
119+
global umb
120+
if msg.get("request_id") == umb.id:
121+
LOGGER.info(f"Received radas response: {msg}")
122+
123+
global result_file
124+
with open(result_file, "w") as f:
125+
json.dump(msg, f)
126+
LOGGER.info(f"Response from radas successfully received for request {umb.id}")
127+
sys.exit(0)
128+
else:
129+
LOGGER.info(f"Ignored message from another request ({msg.get('request_id')})")
130+
131+
132+
def gen_sig_claim_file(reference: str, digest: str, requested_by: str) -> str:
133+
"""
134+
Generated a claim file to be signed based on given data.
135+
Args:
136+
reference: Docker reference for the signed content,
137+
e.g. registry.redhat.io/redhat/community-operator-index:v4.9
138+
digest: Manifest digest for the signed content, usually in the format sha256:xxx
139+
requested_by: Name of the user that requested the signing, for auditing purposes
140+
"""
141+
claim = {
142+
"critical": {
143+
"image": {"docker-manifest-digest": digest},
144+
"type": "atomic container signature",
145+
"identity": {"docker-reference": reference},
146+
},
147+
"optional": {"creator": requested_by},
148+
}
149+
150+
claim = base64.b64encode(json.dumps(claim).encode("utf-8"))
151+
return claim.decode("utf-8")
152+
153+
154+
def gen_image_name(reference: str) -> str:
155+
"""
156+
Generate the image name as a signing input, based on the docker reference.
157+
Args:
158+
reference: Docker reference for the signed content,
159+
e.g. registry.redhat.io/redhat/community-operator-index:v4.9
160+
"""
161+
no_tag = reference.split(":")[0]
162+
image_parts = no_tag.split("/")
163+
return "/".join(image_parts[1:])
164+
165+
166+
def gen_request_msg(args, request_id):
167+
"""
168+
Generate the request message to send to RADAS.
169+
Args:
170+
args: Args from script input.
171+
request_id: UUID to identify match the request with RADAS's response.
172+
173+
Returns:
174+
175+
"""
176+
claim = gen_sig_claim_file(args.reference, args.manifest_digest, args.requester)
177+
image_name = gen_image_name(args.reference)
178+
request_msg = {
179+
"claim_file": claim,
180+
"docker_reference": args.reference,
181+
"image_name": image_name,
182+
"manifest_digest": args.manifest_digest,
183+
"request_id": request_id,
184+
"requested_by": args.requester,
185+
"sig_keyname": args.sig_key_name,
186+
"sig_key_id": args.sig_key_id,
187+
}
188+
return request_msg
189+
190+
191+
def request_signature(args: Any) -> None:
192+
"""
193+
Format and send out a UMB message to request signing, and retry as needed.
194+
"""
195+
global umb
196+
umb = start_umb_client(
197+
hosts=[args.umb_url], client_name=args.umb_client_name, handler=UmbHandler()
198+
)
199+
global result_file
200+
result_file = args.output
201+
202+
request_msg = gen_request_msg(args, umb.id)
203+
204+
umb.connect_and_subscribe(args.umb_listen_topic)
205+
206+
try:
207+
retry_count = 3
208+
for i in range(retry_count + 1):
209+
LOGGER.info(f"Sending signing request message...attempt #{i+1}")
210+
umb.send(args.umb_publish_topic, json.dumps(request_msg))
211+
212+
wait_count = 0
213+
LOGGER.debug(f"Checking for signing response result file {result_file}...")
214+
while not os.path.exists(result_file):
215+
time.sleep(WAIT_INTERVAL_SEC)
216+
wait_count += 1
217+
if wait_count > TIMEOUT_COUNT:
218+
LOGGER.warning("Timeout from waiting for signing response.")
219+
break
220+
else:
221+
# exit retry loop if response file detected
222+
break
223+
224+
LOGGER.info(f"No signing response received. Retrying.")
225+
finally:
226+
# unsubscribe to free up the queue
227+
LOGGER.info("Unsubscribing from queue and disconnecting from UMB...")
228+
umb.unsubscribe(args.umb_listen_topic)
229+
umb.stop()
230+
if not os.path.exists(result_file):
231+
LOGGER.error("No signing response received after all 3 retries.")
232+
sys.exit(1)
233+
234+
235+
def main(): # pragma: no cover
236+
"""
237+
Main func
238+
"""
239+
240+
parser = setup_argparser()
241+
args = parser.parse_args()
242+
log_level = "DEBUG" if args.verbose else "INFO"
243+
setup_logger(log_level)
244+
245+
request_signature(args)
246+
247+
248+
if __name__ == "__main__": # pragma: no cover
249+
main()

0 commit comments

Comments
 (0)