From 5456abff94b46ddc76094a78622b2f762803aa0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Pitucha?= Date: Wed, 15 Jul 2015 20:35:14 +1000 Subject: [PATCH] Add fixups configuration / processing Fixups allow changing the submitted CSR before signing. This may be useful for enforcing rules, like removing deprecated options. All fixups are available in the "anchor.fixups" namespace and each one returns either a new or a modified CSR when it's finished. Partial-bug: #1401580 Change-Id: Id42802194bbdf36799660899eb34f728782bc893 --- README.md | 8 ++ anchor/app.py | 12 +++ anchor/certificate_ops.py | 57 ++++++++++++ anchor/controllers/__init__.py | 1 + anchor/jsonloader.py | 4 + doc/source/configuration.rst | 7 +- doc/source/fixups.rst | 10 +++ doc/source/index.rst | 1 + tests/__init__.py | 5 +- tests/fixups/__init__.py | 0 tests/fixups/test_fixup_functionality.py | 109 +++++++++++++++++++++++ 11 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 doc/source/fixups.rst create mode 100644 tests/fixups/__init__.py create mode 100644 tests/fixups/test_fixup_functionality.py diff --git a/README.md b/README.md index 74c4fb9..0c741eb 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,14 @@ Sample configuration for the default backend: For more information, please refer to the documentation. +Fixups +====== + +Anchor can modify the submitted CSRs in order to enforce some rules, remove +deprecated elements, or just add information. Submitted CSR may be modified or +entirely redone. Fixup are loaded from "anchor.fixups" namespace and can take +parameters just like validators. + Reporting bugs and contributing =============================== diff --git a/anchor/app.py b/anchor/app.py index 152a62e..9f52b64 100644 --- a/anchor/app.py +++ b/anchor/app.py @@ -172,6 +172,18 @@ def validate_registration_authority_config(ra_name, conf): config_check_domains(ra_validators) logger.info("Validators OK for registration authority: %s", ra_name) + ra_fixups = ra_conf.get('fixups', {}) + + for step in ra_fixups.keys(): + try: + jsonloader.conf.get_fixup(step) + except KeyError: + raise ConfigValidationException( + "Unknown fixup <{}> found (for registration " + "authority {})".format(step, ra_name)) + + logger.info("Fixups OK for registration authority: %s", ra_name) + def load_config(): """Attempt to find and load a JSON configuration file. diff --git a/anchor/certificate_ops.py b/anchor/certificate_ops.py index 92c4987..555eb68 100644 --- a/anchor/certificate_ops.py +++ b/anchor/certificate_ops.py @@ -165,6 +165,63 @@ def dispatch_sign(ra_name, csr): return cert_pem +def _run_fixup(name, body, args): + """Parse the fixup tuple, call the fixup, and return the new csr. + + :param name: the fixup name + :param body: fixup body, directly from config + :param args: additional arguments to pass to the fixup function + :return: the fixed csr + """ + # careful to not modify the master copy of args with local params + new_kwargs = args.copy() + new_kwargs.update(body) + + # perform the actual check + logger.debug("_run_fixup: fixup <%s> with arguments: %s", name, body) + try: + fixup = jsonloader.conf.get_fixup(name) + new_csr = fixup(**new_kwargs) + logger.debug("_run_fixup: success: <%s> ", name) + return new_csr + except Exception: + logger.exception("_run_fixup: FAILED: <%s>", name) + return None + + +def fixup_csr(ra_name, csr, request): + """Apply configured changes to the certificate. + + :param ra_name: registration authority name + :param csr: X509 certificate signing request + :param request: pecan request + """ + ra_conf = jsonloader.config_for_registration_authority(ra_name) + args = {'csr': csr, + 'conf': ra_conf, + 'request': request} + + fixups = ra_conf.get('fixups', {}) + try: + for fixup_name, fixup in fixups.items(): + new_csr = _run_fixup(fixup_name, fixup, args) + if new_csr is None: + pecan.abort(500, "Could not finish all required modifications") + if not isinstance(new_csr, signing_request.X509Csr): + logger.error("Fixup %s returned incorrect object", fixup_name) + pecan.abort(500, "Could not finish all required modifications") + args['csr'] = new_csr + + except http_status.HTTPInternalServerError: + raise + + except Exception: + logger.exception("Failed to execute fixups") + pecan.abort(500, "Could not finish all required modifications") + + return args['csr'] + + def sign(csr, ca_conf): """Generate an X.509 certificate and sign it. diff --git a/anchor/controllers/__init__.py b/anchor/controllers/__init__.py index c164b86..ccd2f90 100644 --- a/anchor/controllers/__init__.py +++ b/anchor/controllers/__init__.py @@ -50,6 +50,7 @@ class SignInstanceController(rest.RestController): csr = certificate_ops.parse_csr(pecan.request.POST.get('csr'), pecan.request.POST.get('encoding')) certificate_ops.validate_csr(ra_name, auth_result, csr, pecan.request) + csr = certificate_ops.fixup_csr(ra_name, csr, pecan.request) return certificate_ops.dispatch_sign(ra_name, csr) diff --git a/anchor/jsonloader.py b/anchor/jsonloader.py index 4377c4c..d44f7c4 100644 --- a/anchor/jsonloader.py +++ b/anchor/jsonloader.py @@ -63,6 +63,7 @@ class AnchorConf(): self._validators = stevedore.ExtensionManager("anchor.validators") self._authentication = stevedore.ExtensionManager( "anchor.authentication") + self._fixups = stevedore.ExtensionManager("anchor.fixups") def get_signing_backend(self, name): return self._signing_backends[name].plugin @@ -73,6 +74,9 @@ class AnchorConf(): def get_authentication(self, name): return self._authentication[name].plugin + def get_fixup(self, name): + return self._fixups[name].plugin + @property def config(self): '''Property to return the config dictionary diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 4cdaec7..e03fa93 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -151,6 +151,8 @@ and the list of validators applied to each request. "source_cidrs": { "cidrs": [ "127.0.0.0/8" ] } + }, + "fixups": { } } } @@ -162,7 +164,8 @@ against two validators (``ca_status`` and ``source_cidrs``) and if they pass, the CSR will be signed by the previously defined signing ca called ``local``. Each validator has its own set of parameters described separately in the -:doc:`validators section `. +:doc:`validators section `. Same for fixups described in +:doc:`fixups section ` Example configuration @@ -200,6 +203,8 @@ Example configuration "source_cidrs": { "cidrs": [ "127.0.0.0/8" ] } + }, + "fixups": { } } } diff --git a/doc/source/fixups.rst b/doc/source/fixups.rst new file mode 100644 index 0000000..a5d927c --- /dev/null +++ b/doc/source/fixups.rst @@ -0,0 +1,10 @@ +Fixups +====== + +Fixups can be used to modify submitted CSRs before sigining. That means for +example adding extra name elements, or extensions. Each fixup is loaded from +the "anchor.fixups" namespace using stevedore and gets access to the parsed CSR +and the configuration. + +Unlike validators, each fixup has to return either a new CSR structure or the +modified original. diff --git a/doc/source/index.rst b/doc/source/index.rst index 9d533b9..8262e60 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -17,6 +17,7 @@ Contents: signing_backends ephemeralPKI validators + fixups Indices and tables diff --git a/tests/__init__.py b/tests/__init__.py index 7b64864..24104dc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -55,11 +55,14 @@ class DefaultConfigMixin(object): "allowed_domains": [".test.com"] } } + self.sample_conf_fixups = { + } self.sample_conf_ra = { "default_ra": { "authentication": "default_auth", "signing_ca": "default_ca", - "validators": self.sample_conf_validators + "validators": self.sample_conf_validators, + "fixups": self.sample_conf_fixups, } } self.sample_conf = { diff --git a/tests/fixups/__init__.py b/tests/fixups/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixups/test_fixup_functionality.py b/tests/fixups/test_fixup_functionality.py new file mode 100644 index 0000000..8da7f16 --- /dev/null +++ b/tests/fixups/test_fixup_functionality.py @@ -0,0 +1,109 @@ +# -*- coding:utf-8 -*- +# +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import textwrap +import unittest + +import mock +import webob + +from anchor import certificate_ops +from anchor import jsonloader +from anchor.X509 import signing_request +import tests + + +class TestFixupFunctionality(tests.DefaultConfigMixin, unittest.TestCase): + csr_data_with_cn = textwrap.dedent(u""" + -----BEGIN CERTIFICATE REQUEST----- + MIIDBTCCAe0CAQAwgb8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh + MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMSEwHwYDVQQKExhPcGVuU3RhY2sgU2Vj + dXJpdHkgR3JvdXAxETAPBgNVBAsTCFNlY3VyaXR5MRYwFAYDVQQDEw1vc3NnLnRl + c3QuY29tMTUwMwYJKoZIhvcNAQkBFiZvcGVuc3RhY2stc2VjdXJpdHlAbGlzdHMu + b3BlbnN0YWNrLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJCw + hIh3kwHGrGff7bHpY0x7ebXS8CfnwDx/wFSqlBeARL9f4riN172P4hkk7F+QQ2R9 + 88osQX4dmbQZDX18y85TTQv9jmtzvTZtJM2UQ80XMIVLZjpK5966cmJKqn/s+IaL + zh+kqyb7S6xV0590VarEFZ6JsXdxU9TtVHOWCfn/P8swr5DCTzsE/LUIuVdqgkGh + g63E9iLYtAOUcQv6lpmrI8NHOMK2F7XnP64IEshpZ4POzc7m8nTEHHb0+xxxiive + mwLTp6pyZ5wBx/Dvk2Dc7SF6x51wOxAxdWc3vxwA5Q2nbFK2RlBHCiIi+ZK3i5S/ + tOkcQydQ0Cl9escDrv0CAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQA1dpxxTGFF + TGFenVJlT2uecvXK4UePeaslRx2P1k3xwJK9ZEvKY297cqhK5Y8kWyzNUjGFLHPr + RlgjFMYlUICNgCdcWD2b0avZ9q648+F3b9CWKg0kNMhxyQpXdSeLZOzpDVUyr6TN + GcCZqcQQclixruXsIGQoZFIXazGju2UTtxwK/J87u2S0yR2bR48dPlNXAWKV+e4o + Ua0RaDUUBypZNMMbY6KSB6C7oXGzA/WOnvNz9PzhXlqgWhOv5M6iG3sYDtKllXJT + 7lcLhUzNVdWaPveTqX/V8QX//53IkyNa+IBm+H84UE5M0GFunqFBYqrWw8S46tMQ + JQxgjf65ujnn + -----END CERTIFICATE REQUEST-----""") + """ + Subject: + C=US, ST=California, L=San Francisco, + O=OpenStack Security Group, OU=Security, + CN=ossg.test.com/emailAddress=openstack-security@lists.openstack.org + """ + def setUp(self): + super(TestFixupFunctionality, self).setUp() + jsonloader.conf.load_extensions() + self.csr = signing_request.X509Csr.from_buffer( + TestFixupFunctionality.csr_data_with_cn) + + def test_with_noop(self): + """Ensure single fixup is processed.""" + + self.sample_conf_ra['default_ra']['fixups'] = {'noop': {}} + data = self.sample_conf + + config = "anchor.jsonloader.conf._config" + mock_noop = mock.MagicMock() + mock_noop.name = "noop" + mock_noop.plugin.return_value = self.csr + + jsonloader.conf._fixups = jsonloader.conf._fixups.make_test_instance( + [mock_noop], 'anchor.fixups') + + with mock.patch.dict(config, data): + certificate_ops.fixup_csr('default_ra', self.csr, None) + + mock_noop.plugin.assert_called_with( + csr=self.csr, conf=self.sample_conf_ra['default_ra'], request=None) + + def test_with_no_fixups(self): + """Ensure no fixups is ok.""" + + self.sample_conf_ra['default_ra']['fixups'] = {} + data = self.sample_conf + + config = "anchor.jsonloader.conf._config" + with mock.patch.dict(config, data): + res = certificate_ops.fixup_csr('default_ra', self.csr, None) + self.assertIs(res, self.csr) + + def test_with_broken_fixup(self): + """Ensure broken fixups stop processing.""" + + self.sample_conf_ra['default_ra']['fixups'] = {'broken': {}} + data = self.sample_conf + + config = "anchor.jsonloader.conf._config" + mock_noop = mock.MagicMock() + mock_noop.name = "broken" + mock_noop.plugin.side_effects = Exception("BOOM") + + jsonloader.conf._fixups = jsonloader.conf._fixups.make_test_instance( + [mock_noop], 'anchor.fixups') + + with mock.patch.dict(config, data): + with self.assertRaises(webob.exc.WSGIHTTPException): + certificate_ops.fixup_csr('default_ra', self.csr, None)