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)