06b76aa6e8
Currently etc/barbican/barbican.conf is maintained by hand and can not be regenerated based on the config settings defined in the code. A common pattern for OpenStack projects is to use oslo-config-generator for that task. Co-Authored-By: Randall Burt <randall.burt@rackspace.com> Depends-On: I90870dcb49cd96f6bf0fe353fa6e779ffd87a5af Closes-Bug: #1584789 Change-Id: I5f3dcd2fc982f1178ef7dd662c24d3166f91b266
479 lines
17 KiB
Python
479 lines
17 KiB
Python
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
|
# All Rights Reserved.
|
|
#
|
|
# 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 base64
|
|
import datetime
|
|
import os
|
|
import re
|
|
import subprocess # nosec
|
|
from tempfile import mkstemp
|
|
import uuid
|
|
|
|
from OpenSSL import crypto
|
|
from oslo_config import cfg
|
|
from oslo_utils import fnmatch
|
|
|
|
from barbican.common import config
|
|
from barbican.common import utils
|
|
from barbican import i18n as u
|
|
import barbican.plugin.interface.certificate_manager as cert_manager
|
|
|
|
CONF = config.new_config()
|
|
LOG = utils.getLogger(__name__)
|
|
|
|
|
|
snakeoil_ca_plugin_group = cfg.OptGroup(name='snakeoil_ca_plugin',
|
|
title="Snakeoil CA Plugin Options")
|
|
|
|
snakeoil_ca_plugin_opts = [
|
|
cfg.StrOpt('ca_cert_path',
|
|
help=u._('Path to CA certificate file')),
|
|
cfg.StrOpt('ca_cert_key_path',
|
|
help=u._('Path to CA certificate key file')),
|
|
cfg.StrOpt('ca_cert_chain_path',
|
|
help=u._('Path to CA certificate chain file')),
|
|
cfg.StrOpt('ca_cert_pkcs7_path',
|
|
help=u._('Path to CA chain pkcs7 file')),
|
|
cfg.StrOpt('subca_cert_key_directory',
|
|
default='/etc/barbican/snakeoil-cas',
|
|
help=u._('Directory in which to store certs/keys for subcas')),
|
|
]
|
|
|
|
CONF.register_group(snakeoil_ca_plugin_group)
|
|
CONF.register_opts(snakeoil_ca_plugin_opts, group=snakeoil_ca_plugin_group)
|
|
config.parse_args(CONF)
|
|
|
|
|
|
def list_opts():
|
|
yield snakeoil_ca_plugin_group, snakeoil_ca_plugin_opts
|
|
|
|
|
|
def set_subject_X509Name(target, dn):
|
|
"""Set target X509Name object with parsed dn.
|
|
|
|
This is very basic and should certainly be replaced by something using
|
|
cryptography for instance, but will do for a basic test CA
|
|
"""
|
|
|
|
# TODO(alee) Figure out why C (country) is not working
|
|
fields = dn.split(',')
|
|
for field in fields:
|
|
m = re.search(r"(\w+)\s*=\s*(.+)", field.strip())
|
|
name = m.group(1)
|
|
value = m.group(2)
|
|
if name.lower() == 'ou':
|
|
target.OU = value
|
|
elif name.lower() == 'st':
|
|
target.ST = value
|
|
elif name.lower() == 'cn':
|
|
target.CN = value
|
|
elif name.lower() == 'l':
|
|
target.L = value
|
|
elif name.lower() == 'o':
|
|
target.O = value
|
|
return target
|
|
|
|
|
|
class SnakeoilCA(object):
|
|
|
|
def __init__(self, cert_path=None, key_path=None, chain_path=None,
|
|
pkcs7_path=None, name=None, serial=1,
|
|
key_size=2048, expiry_days=10 * 365, x509_version=2,
|
|
subject_dn=None, signing_dn=None, signing_key=None,
|
|
parent_chain_path=None):
|
|
self.cert_path = cert_path
|
|
self.key_path = key_path
|
|
self.chain_path = chain_path
|
|
self.pkcs7_path = pkcs7_path
|
|
self.name = name
|
|
self.serial = serial
|
|
self.key_size = key_size
|
|
self.expiry_days = expiry_days
|
|
self.x509_version = x509_version
|
|
|
|
self.subject_dn = subject_dn
|
|
|
|
if signing_dn is not None:
|
|
self.signing_dn = signing_dn
|
|
else:
|
|
self.signing_dn = subject_dn # self-signed
|
|
|
|
self.signing_key = signing_key
|
|
self.parent_chain_path = parent_chain_path
|
|
|
|
self._cert_val = None
|
|
self._key_val = None
|
|
self._chain_val = None
|
|
self._pkcs7_val = None
|
|
|
|
@property
|
|
def cert(self):
|
|
self.ensure_exists()
|
|
if self.cert_path:
|
|
with open(self.cert_path, 'rb') as cert_fh:
|
|
return crypto.load_certificate(crypto.FILETYPE_PEM,
|
|
cert_fh.read())
|
|
else:
|
|
return crypto.load_certificate(crypto.FILETYPE_PEM, self._cert_val)
|
|
|
|
@cert.setter
|
|
def cert(self, val):
|
|
if self.cert_path:
|
|
with open(self.cert_path, 'wb') as cert_fh:
|
|
cert_fh.write(crypto.dump_certificate(crypto.FILETYPE_PEM,
|
|
val))
|
|
else:
|
|
self._cert_val = crypto.dump_certificate(crypto.FILETYPE_PEM, val)
|
|
|
|
@property
|
|
def key(self):
|
|
self.ensure_exists()
|
|
if self.key_path:
|
|
with open(self.key_path, 'rb') as key_fh:
|
|
return crypto.load_privatekey(crypto.FILETYPE_PEM,
|
|
key_fh.read())
|
|
else:
|
|
return crypto.load_privatekey(crypto.FILETYPE_PEM, self._key_val)
|
|
|
|
@key.setter
|
|
def key(self, val):
|
|
if self.key_path:
|
|
with open(self.key_path, 'wb') as key_fh:
|
|
key_fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, val))
|
|
else:
|
|
self._key_val = crypto.dump_privatekey(crypto.FILETYPE_PEM, val)
|
|
|
|
@property
|
|
def chain(self):
|
|
self.ensure_exists()
|
|
if self.chain_path:
|
|
with open(self.chain_path, 'rb') as chain_fh:
|
|
return chain_fh.read()
|
|
else:
|
|
return self._chain_val
|
|
|
|
@chain.setter
|
|
def chain(self, val):
|
|
if self.chain_path:
|
|
with open(self.chain_path, 'wb') as chain_fh:
|
|
chain_fh.write(val)
|
|
else:
|
|
self._chain_val = val
|
|
|
|
@property
|
|
def pkcs7(self):
|
|
self.ensure_exists()
|
|
if self.pkcs7_path:
|
|
with open(self.pkcs7_path, 'rb') as pkcs7_fh:
|
|
return pkcs7_fh.read()
|
|
else:
|
|
return self._pkcs7_val
|
|
|
|
@pkcs7.setter
|
|
def pkcs7(self, val):
|
|
if self.pkcs7_path:
|
|
with open(self.pkcs7_path, 'wb') as pkcs7_fh:
|
|
pkcs7_fh.write(val)
|
|
else:
|
|
self._pkcs7_val = val
|
|
|
|
@property
|
|
def exists(self):
|
|
if self.cert_path is not None:
|
|
cert_exists = os.path.isfile(self.cert_path)
|
|
else:
|
|
cert_exists = self._cert_val is not None
|
|
|
|
if self.key_path is not None:
|
|
key_exists = os.path.isfile(self.key_path)
|
|
else:
|
|
key_exists = self._key_val is not None
|
|
|
|
if self.chain_path is not None:
|
|
chain_exists = os.path.isfile(self.chain_path)
|
|
else:
|
|
chain_exists = self._chain_val is not None
|
|
|
|
if self.pkcs7_path is not None:
|
|
pkcs7_exists = os.path.isfile(self.pkcs7_path)
|
|
else:
|
|
pkcs7_exists = self._pkcs7_val is not None
|
|
|
|
return (cert_exists and key_exists and
|
|
pkcs7_exists and chain_exists)
|
|
|
|
def ensure_exists(self):
|
|
if not self.exists:
|
|
LOG.debug('Keypair not found, creating new cert/key')
|
|
self.cert, self.key, self.chain, self.pkcs7 = (
|
|
self.create_keypair())
|
|
|
|
def create_keypair(self):
|
|
LOG.debug('Generating Snakeoil CA')
|
|
key = crypto.PKey()
|
|
key.generate_key(crypto.TYPE_RSA, self.key_size)
|
|
|
|
cert = crypto.X509()
|
|
cert.set_version(self.x509_version)
|
|
cert.set_serial_number(self.serial)
|
|
subject = cert.get_subject()
|
|
set_subject_X509Name(subject, self.subject_dn)
|
|
cert.set_subject(subject)
|
|
cert.gmtime_adj_notBefore(0)
|
|
cert.gmtime_adj_notAfter(self.expiry_days)
|
|
cert.set_issuer(set_subject_X509Name(
|
|
cert.get_issuer(), self.signing_dn))
|
|
cert.set_pubkey(key)
|
|
cert.add_extensions([
|
|
crypto.X509Extension(b"basicConstraints", True,
|
|
b"CA:TRUE, pathlen:5"),
|
|
])
|
|
if not self.signing_key:
|
|
self.signing_key = key # self-signed
|
|
|
|
cert.sign(self.signing_key, 'sha256')
|
|
|
|
LOG.debug('Snakeoil CA cert/key generated')
|
|
|
|
chain = b''
|
|
if self.parent_chain_path:
|
|
with open(self.parent_chain_path, 'rb') as fh:
|
|
chain = fh.read()
|
|
chain += crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
|
|
|
pkcs7 = self._generate_pkcs7(chain)
|
|
return cert, key, chain, pkcs7
|
|
|
|
def _generate_pkcs7(self, chain):
|
|
fin, temp_in = mkstemp()
|
|
os.write(fin, chain)
|
|
os.close(fin)
|
|
|
|
fout, temp_out = mkstemp()
|
|
os.close(fout)
|
|
|
|
subprocess.call(['/usr/bin/openssl', 'crl2pkcs7', '-nocrl', # nosec
|
|
'-out', temp_out, '-certfile', temp_in], shell=False)
|
|
with open(temp_out, 'rb') as pkcs7_fh:
|
|
pkcs7 = pkcs7_fh.read()
|
|
|
|
os.remove(temp_in)
|
|
os.remove(temp_out)
|
|
return pkcs7
|
|
|
|
|
|
class CertManager(object):
|
|
|
|
def __init__(self, ca):
|
|
self.ca = ca
|
|
|
|
def get_new_serial(self):
|
|
return uuid.uuid4().int
|
|
|
|
def make_certificate(self, csr, expires=2 * 365):
|
|
cert = crypto.X509()
|
|
cert.set_serial_number(self.get_new_serial())
|
|
cert.gmtime_adj_notBefore(0)
|
|
cert.gmtime_adj_notAfter(expires)
|
|
cert.set_issuer(self.ca.cert.get_subject())
|
|
cert.set_subject(csr.get_subject())
|
|
cert.set_pubkey(csr.get_pubkey())
|
|
cert.sign(self.ca.key, 'sha256')
|
|
return cert
|
|
|
|
|
|
class SnakeoilCACertificatePlugin(cert_manager.CertificatePluginBase):
|
|
"""Snakeoil CA certificate plugin.
|
|
|
|
This is used for easily generating certificates which are not useful in a
|
|
production environment.
|
|
"""
|
|
|
|
def __init__(self, conf=CONF):
|
|
self.cas = {}
|
|
self.ca = SnakeoilCA(
|
|
cert_path=conf.snakeoil_ca_plugin.ca_cert_path,
|
|
key_path=conf.snakeoil_ca_plugin.ca_cert_key_path,
|
|
chain_path=conf.snakeoil_ca_plugin.ca_cert_chain_path,
|
|
pkcs7_path=conf.snakeoil_ca_plugin.ca_cert_pkcs7_path,
|
|
name=self.get_default_ca_name(),
|
|
subject_dn="cn=Snakeoil Certificate,o=example.com"
|
|
)
|
|
|
|
self.cas[self.get_default_ca_name()] = self.ca
|
|
|
|
self.subca_directory = conf.snakeoil_ca_plugin.subca_cert_key_directory
|
|
if self.subca_directory:
|
|
if not os.path.exists(self.subca_directory):
|
|
os.makedirs(self.subca_directory) # pragma: no cover
|
|
else:
|
|
self._reload_previously_created_subcas()
|
|
|
|
self.cert_manager = CertManager(self.ca)
|
|
|
|
def _reload_previously_created_subcas(self):
|
|
for file in os.listdir(self.subca_directory):
|
|
if fnmatch.fnmatch(file, '*.key'):
|
|
ca_id, _ext = os.path.splitext(file)
|
|
self.cas[ca_id] = SnakeoilCA(
|
|
cert_path=os.path.join(self.subca_directory,
|
|
ca_id + ".cert"),
|
|
key_path=os.path.join(self.subca_directory, file),
|
|
chain_path=os.path.join(self.subca_directory,
|
|
ca_id + ".chain"),
|
|
pkcs7_path=os.path.join(self.subca_directory,
|
|
ca_id + ".p7b")
|
|
)
|
|
|
|
def get_default_ca_name(self):
|
|
return "Snakeoil CA"
|
|
|
|
def get_default_signing_cert(self):
|
|
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.ca.cert)
|
|
|
|
def get_default_intermediates(self):
|
|
return None
|
|
|
|
def supported_request_types(self):
|
|
return [cert_manager.CertificateRequestType.CUSTOM_REQUEST,
|
|
cert_manager.CertificateRequestType.STORED_KEY_REQUEST]
|
|
|
|
def issue_certificate_request(self, order_id, order_meta, plugin_meta,
|
|
barbican_meta_dto):
|
|
if barbican_meta_dto.generated_csr is not None:
|
|
encoded_csr = barbican_meta_dto.generated_csr
|
|
else:
|
|
try:
|
|
encoded_csr = base64.b64decode(order_meta['request_data'])
|
|
except KeyError:
|
|
return cert_manager.ResultDTO(
|
|
cert_manager.CertificateStatus.CLIENT_DATA_ISSUE_SEEN,
|
|
status_message=u._("No request_data specified"))
|
|
csr = crypto.load_certificate_request(crypto.FILETYPE_PEM, encoded_csr)
|
|
|
|
ca_id = barbican_meta_dto.plugin_ca_id
|
|
if ca_id:
|
|
ca = self.cas.get(ca_id)
|
|
if ca is None:
|
|
raise cert_manager.CertificateGeneralException(
|
|
"Invalid ca_id passed into snake oil plugin:" + ca_id)
|
|
else:
|
|
ca = self.ca
|
|
|
|
cert_mgr = CertManager(ca)
|
|
cert = cert_mgr.make_certificate(csr)
|
|
cert_enc = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
|
|
|
return cert_manager.ResultDTO(
|
|
cert_manager.CertificateStatus.CERTIFICATE_GENERATED,
|
|
certificate=base64.b64encode(cert_enc),
|
|
intermediates=base64.b64encode(ca.pkcs7))
|
|
|
|
def modify_certificate_request(self, order_id, order_meta, plugin_meta,
|
|
barbican_meta_dto):
|
|
raise NotImplementedError
|
|
|
|
def cancel_certificate_request(self, order_id, order_meta, plugin_meta,
|
|
barbican_meta_dto):
|
|
raise NotImplementedError
|
|
|
|
def check_certificate_status(self, order_id, order_meta, plugin_meta,
|
|
barbican_meta_dto):
|
|
raise NotImplementedError
|
|
|
|
def supports(self, certificate_spec):
|
|
request_type = certificate_spec.get(
|
|
cert_manager.REQUEST_TYPE,
|
|
cert_manager.CertificateRequestType.CUSTOM_REQUEST)
|
|
return request_type in self.supported_request_types()
|
|
|
|
def supports_create_ca(self):
|
|
return True
|
|
|
|
def create_ca(self, ca_create_dto):
|
|
# get the parent CA from the ca list, return error if not on list
|
|
parent_ca_id = ca_create_dto.parent_ca_id
|
|
if not parent_ca_id:
|
|
raise cert_manager.CertificateGeneralException(
|
|
"No parent id passed to snake oil plugin on create_ca")
|
|
|
|
parent_ca = self.cas.get(parent_ca_id)
|
|
if not parent_ca:
|
|
raise cert_manager.CertificateGeneralException(
|
|
"Invalid parent id passed to snake oil plugin:" + parent_ca_id)
|
|
|
|
# create a new ca, passing in key and issuer from the parent
|
|
new_ca_id = str(uuid.uuid4())
|
|
new_cert_path = os.path.join(self.subca_directory, new_ca_id + ".cert")
|
|
new_key_path = os.path.join(self.subca_directory, new_ca_id + ".key")
|
|
new_chain_path = os.path.join(self.subca_directory,
|
|
new_ca_id + ".chain")
|
|
new_pkcs7_path = os.path.join(self.subca_directory,
|
|
new_ca_id + ".p7b")
|
|
parent_chain_path = parent_ca.chain_path
|
|
|
|
new_ca = SnakeoilCA(cert_path=new_cert_path,
|
|
key_path=new_key_path,
|
|
chain_path=new_chain_path,
|
|
pkcs7_path=new_pkcs7_path,
|
|
name=ca_create_dto.name,
|
|
subject_dn=ca_create_dto.subject_dn,
|
|
signing_dn=parent_ca.subject_dn,
|
|
signing_key=parent_ca.key,
|
|
parent_chain_path=parent_chain_path)
|
|
|
|
self.cas[new_ca_id] = new_ca
|
|
|
|
expiration = (datetime.datetime.utcnow() + datetime.timedelta(
|
|
days=cert_manager.CA_INFO_DEFAULT_EXPIRATION_DAYS))
|
|
|
|
return {
|
|
cert_manager.INFO_NAME: new_ca.name,
|
|
cert_manager.INFO_CA_SIGNING_CERT: crypto.dump_certificate(
|
|
crypto.FILETYPE_PEM, new_ca.cert),
|
|
cert_manager.INFO_EXPIRATION: expiration.isoformat(),
|
|
cert_manager.INFO_INTERMEDIATES: new_ca.pkcs7,
|
|
cert_manager.PLUGIN_CA_ID: new_ca_id
|
|
}
|
|
|
|
def get_ca_info(self):
|
|
expiration = (datetime.datetime.utcnow() + datetime.timedelta(
|
|
days=cert_manager.CA_INFO_DEFAULT_EXPIRATION_DAYS))
|
|
|
|
ret = {}
|
|
for ca_id, ca in self.cas.items():
|
|
ca_info = {
|
|
cert_manager.INFO_NAME: ca.name,
|
|
cert_manager.INFO_CA_SIGNING_CERT: crypto.dump_certificate(
|
|
crypto.FILETYPE_PEM, ca.cert),
|
|
cert_manager.INFO_INTERMEDIATES: ca.pkcs7,
|
|
cert_manager.INFO_EXPIRATION: expiration.isoformat()
|
|
}
|
|
ret[ca_id] = ca_info
|
|
|
|
return ret
|
|
|
|
def delete_ca(self, ca_id):
|
|
self.cas.pop(ca_id)
|
|
|
|
ca_files = [os.path.join(self.subca_directory, ca_id + ".cert"),
|
|
os.path.join(self.subca_directory, ca_id + ".key"),
|
|
os.path.join(self.subca_directory, ca_id + ".chain"),
|
|
os.path.join(self.subca_directory, ca_id + ".p7b")]
|
|
|
|
for ca_file in ca_files:
|
|
if os.path.exists(ca_file):
|
|
os.remove(ca_file)
|