anchor/anchor/certificate_ops.py

202 lines
6.6 KiB
Python

#
# 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.
from __future__ import absolute_import
import logging
import os
import pecan
from webob import exc as http_status
from anchor import cmc
from anchor import jsonloader
from anchor import util
from anchor import validation
from anchor.X509 import certificate
from anchor.X509 import signing_request
logger = logging.getLogger(__name__)
# we only support the PEM encoding for now, but this may grow
# to support things like DER in the future
VALID_ENCODINGS = ['pem']
def parse_csr(data, encoding):
"""Loads the user provided CSR into the backend X509 library.
:param data: CSR as provided by the API user
:param encoding: encoding for the CSR (must be PEM today)
:return: CSR object from backend X509 library or aborts
"""
# validate untrusted input
if str(encoding).lower() not in VALID_ENCODINGS:
logger.error("parse_csr failed: bad encoding ({})".format(encoding))
pecan.abort(400, "invalid CSR")
if data is None:
logger.error("parse_csr failed: missing CSR")
pecan.abort(400, "invalid CSR")
# get DER version
der = util.extract_pem(data.encode('ascii'))
if der is None:
logger.error("parse_csr failed: PEM contentents not found")
pecan.abort(400, "PEM contents not found")
# try to unpack the certificate from CMC wrappers
try:
csr = cmc.parse_request(der)
return signing_request.X509Csr(csr)
except cmc.CMCParsingError:
# it's not CMC data, that's fine, it's likely the CSR itself
try:
return signing_request.X509Csr.from_buffer(der, 'der')
except Exception as e:
logger.exception("Exception while parsing the CSR: %s", e)
pecan.abort(400, "CSR cannot be parsed")
def validate_csr(ra_name, auth_result, csr, request):
"""Validates various aspects of the CSR based on the loaded config.
The arguments of this method are passed to the underlying validate
methods. Therefore, some may be optional, depending on which
validation routines are specified in the configuration.
:param ra_name: name of the registration authority
:param auth_result: AuthDetails value from auth.validate
:param csr: CSR value from certificate_ops.parse_csr
:param request: pecan request object associated with this action
"""
try:
valid = validation.validate_csr(ra_name, auth_result, csr, request)
except Exception as e:
logger.exception("Error running validators: %s", e)
pecan.abort(500, "Internal Validation Error")
if not all(list(valid.values())):
pecan.abort(400, "CSR failed validation")
def certificate_fingerprint(cert_pem, hash_name):
"""Get certificate fingerprint."""
cert = certificate.X509Certificate.from_buffer(cert_pem)
return cert.get_fingerprint(hash_name)
def get_ca(ra_name):
ca_conf = jsonloader.signing_ca_for_registration_authority(ra_name)
ca_path = ca_conf.get('cert_path')
if not ca_path:
pecan.abort(404, "CA certificate not available")
try:
with open(ca_path) as f:
return f.read()
except IOError:
pecan.abort(500, "CA certificate not available")
def dispatch_sign(ra_name, csr):
"""Dispatch the sign call to the configured backend.
:param csr: X509 certificate signing request
:return: signed certificate in PEM format
"""
ca_conf = jsonloader.signing_ca_for_registration_authority(ra_name)
backend_name = ca_conf.get('backend', 'anchor')
sign_func = jsonloader.conf.get_signing_backend(backend_name)
try:
cert_pem = sign_func(csr, ca_conf)
except http_status.HTTPException:
logger.exception("Failed to sign certificate")
raise
except Exception:
logger.exception("Failed to sign the certificate")
pecan.abort(500, "certificate signing error")
fingerprint = certificate_fingerprint(cert_pem, 'sha256')
if ca_conf.get('output_path') is not None:
path = os.path.join(
ca_conf['output_path'],
'%s.crt' % fingerprint)
logger.info("Saving certificate to: %s", path)
with open(path, "w") as f:
f.write(cert_pem)
return cert_pem, fingerprint
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']