Skip to content

Commit 9d392bb

Browse files
authored
Added batch mgs signer (#53)
* Added batch mgs signer New sub type of msg signer allows sending signing requests in batches in on messaging message * Changed sig_key_id to sig_key_ids * use only last 8 characters of sig_key_ids * Reformated
1 parent 69a58c4 commit 9d392bb

File tree

9 files changed

+1156
-34
lines changed

9 files changed

+1156
-34
lines changed

mkdocs/docs/user-guide/user-guide.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,5 @@ Before you start reading further:
88
Bellow you find details about supported signatures and signers. How they work and how you can validate
99
signatures produces by them.
1010

11-
## Clearsign
12-
13-
### Messaging signer
14-
15-
Messaging signer works as client which communicates with the server via messaging bus. User data
16-
to be signed are wrapped into signing requests and sent to the server. The server replies with
17-
signed requests which are composed from the original signing request and the signature. Signature
18-
is base64 encoded clearsign of user data.
11+
* [[clear-sign|Clearsign]]
12+
* [[container-signing|Containers Signing]]

mkdocs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ nav:
1111
- 'user-guide/config.md'
1212
- 'user-guide/cli-commands.md'
1313
- 'user-guide/container-signing.md'
14+
- 'user-guide/clear-sign.md'
1415
- 'user-guide/signers/cosign-signer.md'
1516
- 'user-guide/signers/msg-signer.md'
1617
- 'developer/developer-guide.md'

requirements.txt

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1-
requests
2-
typing-extensions
3-
mock
4-
5-
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.12
3+
# by the following command:
4+
#
5+
# pip-compile
6+
#
7+
certifi==2025.6.15
8+
# via requests
9+
charset-normalizer==3.4.2
10+
# via requests
11+
idna==3.10
12+
# via requests
13+
mock==5.2.0
14+
# via -r requirements.in
15+
requests==2.32.4
16+
# via -r requirements.in
17+
typing-extensions==4.14.1
18+
# via -r requirements.in
19+
urllib3==2.5.0
20+
# via requests

src/pubtools/sign/conf/conf.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88

99
class MsgSignerSchema(ma.Schema):
10-
"""Radas signer configuration schema."""
10+
"""Msg signer configuration schema."""
1111

1212
messaging_brokers = ma.fields.List(ma.fields.String(), required=True)
1313
messaging_cert_key = ma.fields.String(required=True)
@@ -24,6 +24,12 @@ class MsgSignerSchema(ma.Schema):
2424
key_aliases = ma.fields.Dict(required=False, keys=ma.fields.String(), values=ma.fields.String())
2525

2626

27+
class MsgBatchSignerSchema(MsgSignerSchema):
28+
"""Msg batch signer configuration schema."""
29+
30+
chunk_size = ma.fields.Integer(required=False)
31+
32+
2733
class CosignSignerSchema(ma.Schema):
2834
"""Cosign signer configuration schema."""
2935

@@ -46,6 +52,7 @@ class ConfigSchema(ma.Schema):
4652
"""pubtools-sign configuration schema."""
4753

4854
msg_signer = ma.fields.Nested(MsgSignerSchema)
55+
msg_batch_signer = ma.fields.Nested(MsgBatchSignerSchema)
4956
cosign_signer = ma.fields.Nested(CosignSignerSchema)
5057

5158

src/pubtools/sign/signers/msgsigner.py

Lines changed: 203 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,26 @@ def create_manifest_claim_message(digest: str, reference: str) -> str:
448448
}
449449
return base64.b64encode(json.dumps(manifest_claim).encode("latin1")).decode("latin1")
450450

451+
def _prepare_messages(self, operation: ContainerSignOperation) -> List[List[MsgMessage]]:
452+
fargs = []
453+
for digest, reference in zip(operation.digests, operation.references):
454+
repo = reference.split("/", 1)[1].split(":")[0]
455+
fargs.append(
456+
FData(
457+
args=[
458+
self.create_manifest_claim_message(digest=digest, reference=reference),
459+
repo,
460+
operation,
461+
SignRequestType.CONTAINER,
462+
],
463+
kwargs={
464+
"extra_attrs": {"pub_task_id": operation.task_id, "manifest_digest": digest}
465+
},
466+
)
467+
)
468+
ret = run_in_parallel(self._create_msg_messages, fargs)
469+
return list(ret.values())
470+
451471
def container_sign(self: MsgSigner, operation: ContainerSignOperation) -> SigningResults:
452472
"""Run container signing operation.
453473
@@ -476,24 +496,9 @@ def container_sign(self: MsgSigner, operation: ContainerSignOperation) -> Signin
476496

477497
LOG.info(f"Container sign operation for {len(operation.digests)}")
478498

479-
fargs = []
480-
for digest, reference in zip(operation.digests, operation.references):
481-
repo = reference.split("/", 1)[1].split(":")[0]
482-
fargs.append(
483-
FData(
484-
args=[
485-
self.create_manifest_claim_message(digest=digest, reference=reference),
486-
repo,
487-
operation,
488-
SignRequestType.CONTAINER,
489-
],
490-
kwargs={
491-
"extra_attrs": {"pub_task_id": operation.task_id, "manifest_digest": digest}
492-
},
493-
)
494-
)
495-
ret = run_in_parallel(self._create_msg_messages, fargs)
496-
for n, _key_messages in ret.items():
499+
ret = self._prepare_messages(operation)
500+
501+
for _key_messages in ret:
497502
for message in _key_messages:
498503
message_to_data[message.body["request_id"]] = message
499504
messages.append(message)
@@ -613,6 +618,175 @@ def container_sign(self: MsgSigner, operation: ContainerSignOperation) -> Signin
613618
return signing_results
614619

615620

621+
class MsgBatchSigner(MsgSigner):
622+
"""Messaging batch signer class."""
623+
624+
_signer_config_key: str = "msg_batch_signer"
625+
626+
chunk_size: int = field(
627+
init=False,
628+
metadata={
629+
"description": "Identify how many signing claims should be send in one message",
630+
"sample": 10,
631+
},
632+
)
633+
634+
SUPPORTED_OPERATIONS: ClassVar[List[Type[SignOperation]]] = [
635+
ContainerSignOperation,
636+
]
637+
638+
def _construct_signing_batch_message(
639+
self: Self,
640+
claims: List[str],
641+
signing_keys: List[str],
642+
repo: str,
643+
signing_key_names: List[str] = [],
644+
extra_attrs: Optional[Dict[str, Any]] = None,
645+
sig_type: str = SignRequestType.CONTAINER,
646+
) -> dict[str, Any]:
647+
data_attr = "claims" if sig_type == SignRequestType.CONTAINER else "data"
648+
_extra_attrs = extra_attrs or {}
649+
processed_claims = [
650+
{
651+
"claim_file": claim,
652+
"sig_keyname": signing_key_names,
653+
"sig_key_ids": [sig_key[-8:] for sig_key in signing_keys],
654+
"manifest_digest": digest,
655+
}
656+
for claim, digest in zip(claims, _extra_attrs.get("manifest_digest", ""))
657+
]
658+
message = {
659+
data_attr: processed_claims,
660+
"request_id": str(uuid.uuid4()),
661+
"created": isodate_now(),
662+
"requested_by": self.creator,
663+
"repo": repo,
664+
}
665+
_extra_attrs.pop("manifest_digest", None)
666+
message.update(_extra_attrs)
667+
return message
668+
669+
def _create_msg_batch_message(
670+
self: Self,
671+
data: List[str],
672+
repo: str,
673+
operation: SignOperation,
674+
sig_type: SignRequestType,
675+
extra_attrs: Optional[Dict[str, Any]] = None,
676+
) -> List[MsgMessage]:
677+
messages = []
678+
signing_keys = []
679+
for _signing_key in operation.signing_keys:
680+
if _signing_key in self.key_aliases:
681+
signing_keys.append(self.key_aliases[_signing_key])
682+
LOG.info(
683+
f"Using signing key alias {self.key_aliases[_signing_key]} for {_signing_key}"
684+
)
685+
else:
686+
signing_keys.append(_signing_key)
687+
688+
extra_attrs = extra_attrs or {}
689+
headers = self._construct_headers(sig_type, extra_attrs=extra_attrs)
690+
if isinstance(operation, ContainerSignOperation):
691+
extra_attrs["manifest_digest"] = operation.digests
692+
ret = MsgMessage(
693+
headers=headers,
694+
body=self._construct_signing_batch_message(
695+
data,
696+
signing_keys,
697+
repo,
698+
signing_key_names=(
699+
operation.signing_key_names
700+
if operation.signing_key_names
701+
else ["" * len(signing_keys)]
702+
),
703+
extra_attrs=extra_attrs,
704+
sig_type=sig_type.value,
705+
),
706+
address=self.topic_send_to.format(
707+
**dict(list(asdict(self).items()) + list(asdict(operation).items()))
708+
),
709+
)
710+
LOG.debug(f"Construted message with request_id {ret.body['request_id']}")
711+
messages.append(ret)
712+
return messages
713+
714+
def _prepare_messages(self: Self, operation: ContainerSignOperation) -> List[List[MsgMessage]]:
715+
messages: List[List[MsgMessage]] = []
716+
repo_groups: Dict[str, Dict[str, List[str]]] = {}
717+
for digest, reference in zip(operation.digests, operation.references):
718+
repo = reference.split("/", 1)[1].split(":")[0]
719+
if repo not in repo_groups:
720+
repo_groups[repo] = cast(dict[str, list[str]], {"digests": [], "references": []})
721+
repo_groups[repo]["digests"].append(digest)
722+
repo_groups[repo]["references"].append(reference)
723+
724+
batch_data: List[FData] = []
725+
for repo, group in repo_groups.items():
726+
claims = []
727+
digests = []
728+
729+
for digest, reference in zip(group["digests"], group["references"]):
730+
claims.append(
731+
self.create_manifest_claim_message(digest=digest, reference=reference)
732+
)
733+
digests.append(digest)
734+
if len(claims) >= self.chunk_size:
735+
fdata = FData(
736+
args=[claims, repo, operation, SignRequestType.CONTAINER],
737+
kwargs={
738+
"extra_attrs": {
739+
"pub_task_id": operation.task_id,
740+
"manifest_digest": digests,
741+
}
742+
},
743+
)
744+
batch_data.append(fdata)
745+
claims = []
746+
digests = []
747+
if claims:
748+
fdata = FData(
749+
args=[claims, repo, operation, SignRequestType.CONTAINER],
750+
kwargs={
751+
"extra_attrs": {
752+
"pub_task_id": operation.task_id,
753+
"manifest_digest": digests,
754+
}
755+
},
756+
)
757+
batch_data.append(fdata)
758+
759+
ret = run_in_parallel(self._create_msg_batch_message, batch_data)
760+
messages.extend(list(ret.values()))
761+
return messages
762+
763+
def load_config(self: Self, config_data: Dict[str, Any]) -> None:
764+
"""Load configuration of messaging signer.
765+
766+
Arguments:
767+
config_data (dict): configuration data to load
768+
"""
769+
self.messaging_brokers = config_data["msg_batch_signer"]["messaging_brokers"]
770+
self.messaging_cert_key = os.path.expanduser(
771+
config_data["msg_batch_signer"]["messaging_cert_key"]
772+
)
773+
self.messaging_ca_cert = os.path.expanduser(
774+
config_data["msg_batch_signer"]["messaging_ca_cert"]
775+
)
776+
self.topic_send_to = config_data["msg_batch_signer"]["topic_send_to"]
777+
self.topic_listen_to = config_data["msg_batch_signer"]["topic_listen_to"]
778+
self.environment = config_data["msg_batch_signer"]["environment"]
779+
self.service = config_data["msg_batch_signer"]["service"]
780+
self.message_id_key = config_data["msg_batch_signer"]["message_id_key"]
781+
self.retries = config_data["msg_batch_signer"]["retries"]
782+
self.send_retries = config_data["msg_batch_signer"]["send_retries"]
783+
self.log_level = config_data["msg_batch_signer"]["log_level"]
784+
self.timeout = config_data["msg_batch_signer"]["timeout"]
785+
self.creator = self._get_cert_subject_cn()
786+
self.key_aliases = config_data["msg_batch_signer"].get("key_aliases", {})
787+
self.chunk_size = config_data["msg_batch_signer"]["chunk_size"]
788+
789+
616790
def msg_clear_sign(
617791
inputs: List[str],
618792
signing_keys: List[str] = [],
@@ -671,9 +845,14 @@ def msg_container_sign(
671845
digest: list[str] = [],
672846
reference: list[str] = [],
673847
requester: str = "",
848+
signer_type: str = "single",
674849
) -> Dict[str, Any]:
675850
"""Run containersign operation with cli arguments."""
676-
msg_signer = MsgSigner()
851+
if signer_type == "single":
852+
msg_signer = MsgSigner()
853+
elif signer_type == "batch":
854+
msg_signer = MsgBatchSigner()
855+
677856
config = _get_config_file(config_file)
678857
msg_signer.load_config(load_config(os.path.expanduser(config)))
679858
if requester:
@@ -819,6 +998,9 @@ def msg_clear_sign_main(
819998
default="INFO",
820999
help="Set log level",
8211000
)
1001+
@click.option(
1002+
"--signer-type", type=click.Choice(["single", "batch"]), default="single", help="Signer type"
1003+
)
8221004
def msg_container_sign_main(
8231005
signing_key: List[str] = [],
8241006
signing_key_name: List[str] = [],
@@ -829,6 +1011,7 @@ def msg_container_sign_main(
8291011
requester: str = "",
8301012
raw: bool = False,
8311013
log_level: str = "INFO",
1014+
signer_type: str = "single",
8321015
) -> None:
8331016
"""Entry point method for containersign operation.
8341017
@@ -856,6 +1039,7 @@ def msg_container_sign_main(
8561039
digest=digest,
8571040
reference=reference,
8581041
requester=requester,
1042+
signer_type=signer_type,
8591043
)
8601044
if not raw:
8611045
click.echo(json.dumps(ret))

src/pubtools/sign/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ class FData:
107107

108108

109109
def run_in_parallel(
110-
func: Callable[..., Any], data: List[FData], threads: int = 10
110+
func: Callable[..., Any], data: Iterable[FData], threads: int = 10
111111
) -> Dict[Any, Any]:
112112
"""Run method on data in parallel.
113113

tests/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
f_config_msg_signer_ok, # noqa: F401
1717
f_config_msg_signer_ok2, # noqa: F401
1818
f_config_msg_signer_aliases, # noqa: F401
19+
f_msg_batch_signer, # noqa: F401
20+
f_config_msg_batch_signer_ok, # noqa: F401
21+
f_config_msg_batch_signer_aliases, # noqa: F401
22+
f_config_msg_batch_signer_ok2, # noqa: F401
1923
) # noqa: F401
2024
from .conftest_cosignsig import ( # noqa: F401
2125
f_cosign_signer, # noqa: F401

0 commit comments

Comments
 (0)