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
This commit is contained in:
parent
222684e825
commit
5456abff94
@ -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
|
||||
===============================
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 </validators>`.
|
||||
:doc:`validators section </validators>`. Same for fixups described in
|
||||
:doc:`fixups section </fixups>`
|
||||
|
||||
|
||||
Example configuration
|
||||
@ -200,6 +203,8 @@ Example configuration
|
||||
"source_cidrs": {
|
||||
"cidrs": [ "127.0.0.0/8" ]
|
||||
}
|
||||
},
|
||||
"fixups": {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
10
doc/source/fixups.rst
Normal file
10
doc/source/fixups.rst
Normal file
@ -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.
|
@ -17,6 +17,7 @@ Contents:
|
||||
signing_backends
|
||||
ephemeralPKI
|
||||
validators
|
||||
fixups
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
@ -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 = {
|
||||
|
0
tests/fixups/__init__.py
Normal file
0
tests/fixups/__init__.py
Normal file
109
tests/fixups/test_fixup_functionality.py
Normal file
109
tests/fixups/test_fixup_functionality.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user