From fb9a3e398a32cc4ab1acf1d5b36090dc69306d2e Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Jan 21 2022 20:51:44 +0000 Subject: Add XML signing support This adds support for signing XML files. Signed-off-by: Demi Marie Obenour --- diff --git a/robosignatory.toml b/robosignatory.toml index f4fadd9..28c1a7b 100644 --- a/robosignatory.toml +++ b/robosignatory.toml @@ -20,6 +20,7 @@ routing_keys = [ "org.fedoraproject.*.pungi.compose.ostree", "org.fedoraproject.*.coreos.build.request.artifacts-sign", "org.fedoraproject.*.coreos.build.request.ostree-sign", + "org.fedoraproject.*.robosignatory.xml-sign", "org.fedoraproject.*.buildsys.tag", ] diff --git a/robosignatory/consumer.py b/robosignatory/consumer.py index 662728d..e8ff2fd 100644 --- a/robosignatory/consumer.py +++ b/robosignatory/consumer.py @@ -7,6 +7,7 @@ import fedora_messaging from .tag import TagSigner from .atomic import AtomicSigner from .coreos import CoreOSSigner +from .xml import XMLSigner log = logging.getLogger('robosignatory') @@ -21,6 +22,7 @@ class Consumer(object): self.tag_handler = TagSigner(self.config) self.atomic_handler = AtomicSigner(self.config) self.coreos_handler = CoreOSSigner(self.config) + self.xml_handler = XMLSigner(self.config) def __call__(self, msg): """ @@ -49,6 +51,9 @@ class Consumer(object): or msg.topic.endswith('.coreos.build.request.ostree-sign')): log.debug('Passing message to the CoreOS handler') self.coreos_handler.consume(msg) + elif msg.topic.endswith('.robosignatory.xml-sign'): + log.debug('Passing message to the Text handler') + self.xml_handler.consume(msg) except Exception as e: error_msg = '{e}: Unable to handle message: {msg}'.format(e=e, msg=msg) log.exception(error_msg) diff --git a/robosignatory/utils.py b/robosignatory/utils.py index fbcb651..5326f58 100644 --- a/robosignatory/utils.py +++ b/robosignatory/utils.py @@ -90,6 +90,10 @@ class BaseSigningHelper(object): def build_coreos_cmdline(self, *args): pass + @abc.abstractmethod + def build_xml_cmdline(self, *args): + pass + class EchoHelper(BaseSigningHelper): """ A dummy "hello world" helper, used for debugging. """ @@ -116,6 +120,11 @@ class EchoHelper(BaseSigningHelper): log.info(result) return result + def build_xml_cmdline(self, *args, **kwargs): + result = ['echo', ' '.join(['build_xml_cmdline:', str(args), str(kwargs)])] + log.info(result) + return result + class SigulHelper(BaseSigningHelper): def __init__(self, user, passphrase_file, config_file=None, @@ -164,11 +173,16 @@ class SigulHelper(BaseSigningHelper): return command + rpms def build_atomic_cmdline(self, key, checksum, input_file, output_file): - command = self.build_cmdline('sign-ostree', key, checksum, input_file, - '--output', output_file) + command = self.build_cmdline('sign-ostree', '--output', output_file, + '--', key, checksum, input_file) return command def build_coreos_cmdline(self, key, input_file, output_file): - command = self.build_cmdline('sign-data', key, input_file, - '--output', output_file) + command = self.build_cmdline('sign-data', '--output', output_file, '--', + key, input_file) + return command + + def build_xml_cmdline(self, key, input_file, output_file): + command = self.build_cmdline('sign-data', '--output', output_file, '--', + key, input_file) return command diff --git a/robosignatory/xml.py b/robosignatory/xml.py new file mode 100644 index 0000000..31b5cf6 --- /dev/null +++ b/robosignatory/xml.py @@ -0,0 +1,87 @@ +from __future__ import unicode_literals, absolute_import + +import os +import stat +import logging +import shutil +import tempfile + +import robosignatory.utils as utils +from fedora_messaging.api import Message, publish + + +log = logging.getLogger(__name__) + +class XMLSigner(object): + + __slots__ = ('_signer', '_tmpdir', '_key') + + def __init__(self, config): + self._signer = utils.get_signing_helper(**config['signing']) + self._tmpdir = '/tmp' + for k, v in config['xml'].items(): + if k == 'tmpdir' and type(v) is str: + self._tmpdir = v + elif k == 'key' and type(v) is str: + self._key = v + else: + raise Exception('Bad config entry {!r}'.format((k, v))) + if not hasattr(self, '_key'): + raise Exception('Key must be specified') + log.info('XMLSigner ready for service') + + def _sign_object(self, msg, tmpdir): + input_file = os.path.join(tmpdir, 'input') + output_file = os.path.join(tmpdir, 'output') + with open(input_file, 'wb') as f: + f.write(msg.encode('ascii', 'strict')) + cmdline = self._signer.build_xml_cmdline(self._key, input_file, output_file) + ret, stdout, stderr = utils.run_command(cmdline) + if ret != 0: + raise Exception('Error signing! Signing output: %s, stdout: ' + '%r, stderr: %r' % (ret, stdout, stderr)) + with open(output_file, 'rb') as f: + signature = f.read().decode('ascii', 'strict') + log.info('XML file was successfully signed') + return signature + + def _error(self, msg, error): + publish(Message( + topic="{}.finished".format(msg.topic), + # respond with the same body + body={'body': msg.body, 'error': str(error)} + )) + + + def consume(self, msg): + log.info('Punji and/or bodhi wants to sign an XML file') + + if type(msg.body) is not str: + return self._error(msg, 'Refusing to sign non-string') + # Security check: we must use the same key for signing packages and + # metadata, but being authorized to sign metadata does not necessarily + # grant authorization to sign packages. This check enforces this. + # + # RPM packages have two possible signatures: one over just the main + # header, and one over both the main header and the payload. RPM checks + # that the main header header begins with + # b"\x8e\xad\xe8\x01\x00\x00\x00\x00", so we’re in the clear: it is + # not possible to abuse this endpoint to sign an RPM. + if not msg.body.startswith('') + +INVALID_MESSAGE = Message( + topic="org.fedoraproject.prod.robosignatory.xml-sign", + body='\0') + +class TestXML(unittest.TestCase): + + def setUp(self): + self.consumer = XMLSigner(TEST_CONFIG) + + def _get_response_message(self, source_msg, failed=False, failure_msg="Signing failed", sig=""): + body= { + 'body': source_msg.body, + 'error': failure_msg, + } if failed else { + 'body': source_msg.body, + 'signature': sig, + } + return Message( + topic=source_msg.topic + ".finished", + body=body + ) + + @mock.patch('robosignatory.xml.utils.run_command') + def test_xml_sign(self, run_command): + run_command.return_value = 0, "", "" + expected_response = self._get_response_message(XML_MESSAGE, failed=True) + with mock_sends(expected_response): + self.consumer.consume(XML_MESSAGE) + run_command.assert_called() + + @mock.patch('robosignatory.xml.utils.run_command') + def test_wrong_xml(self, run_command): + run_command.return_value = 0, "", "" + expected_response = self._get_response_message(INVALID_MESSAGE, + failed=True, failure_msg='Refusing to sign object that does not ' + 'start with XML declaration') + with mock_sends(expected_response): + self.consumer.consume(INVALID_MESSAGE) + run_command.assert_not_called() + + @mock.patch('robosignatory.coreos.utils.run_command') + def test_signing_failed(self, run_command): + run_command.return_value = 1, "stdout", "stderr" + expected_response = self._get_response_message(XML_MESSAGE, failed=True) + with mock_sends(expected_response): + self.consumer.consume(XML_MESSAGE) + + run_command.assert_called() + + @mock.patch('robosignatory.coreos.utils.run_command') + def test_no_signature(self, run_command): + run_command.return_value = 0, "stdout", "stderr" + expected_response = self._get_response_message(XML_MESSAGE, failed=True) + + with mock_sends(expected_response): + self.consumer.consume(XML_MESSAGE) + + run_command.assert_called()