ca.py 6.98 KB
Newer Older
ale's avatar
ale committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
import errno
import fcntl
import logging
import re
import os
import getpass
import random
import shutil
import tempfile
import time
from cam import openssl_wrap
from cam import utils

log = logging.getLogger(__name__)


class _CAFiles(object):

    def __init__(self, basedir, **attrs):
        for key, value in attrs.items():
            setattr(self, key, os.path.join(basedir, value))


class CA(object):

    def __init__(self, basedir, config, password=None):
        self._pw = password
        self.basedir = basedir
        self.config = {'basedir': basedir, 'default_days': '365', 'ou': 'CA',
                       'days': '3650', 'country': 'XX', 'crl_url': '',
ale's avatar
ale committed
31
                       'signature_algorithm': 'sha1', 'bits': '2048'}
ale's avatar
ale committed
32 33 34 35 36
        self.config.update(config)
        self.files = _CAFiles(basedir, 
                              conf='conf/ca.conf',
                              public_key='public/ca.pem',
                              private_key='private/ca.key',
37
                              crl='public/ca.crl',
ale's avatar
ale committed
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
                              serial='serial',
                              crlnumber='crlnumber',
                              index='index')
        self._lock()

    def _getpw(self):
        if self._pw is None:
            self._pw = getpass.getpass(prompt='CA Password: ')
        return self._pw

    def _lock(self):
        self._lockfd = open(os.path.join(self.basedir, '_lock'), 'w+')
        n = 3
        while True:
            try:
                fcntl.lockf(self._lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
                break
            except IOError, e:
                if e.errno in (errno.EACCES, errno.EAGAIN):
                    n -= 1
                    if n == 0:
                        log.error('another instance is running')
                        raise
                    time.sleep(1)
                    continue
                raise

    def _unlock(self):
        fcntl.lockf(self._lockfd, fcntl.LOCK_UN)
        self._lockfd.close()

69 70 71 72
    def _update_config(self):
        # Create the OpenSSL configuration file.
        utils.render(self.files.conf, 'openssl_config', self.config)

ale's avatar
ale committed
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
    def close(self):
        self._unlock()

    def create(self):
        old_umask = os.umask(077)

        for pathext in ('', 'conf', 'public', 'public/certs',
                        'public/crl', 'private', 'newcerts'):
            fullpath = os.path.join(self.basedir, pathext)
            if not os.path.isdir(fullpath):
                os.mkdir(fullpath)

        if not os.path.exists(self.files.index):
            log.info('creating new index file')
            open(self.files.index, 'w').close()

        if not os.path.exists(self.files.serial):
            serial = random.randint(1, 1000000000)
            log.info('initializing serial number (%d)', serial)
            with open(self.files.serial, 'w') as fd:
                fd.write('%08X\n' % serial)

        if not os.path.exists(self.files.crlnumber):
            with open(self.files.crlnumber, 'w') as fd:
                fd.write('01\n')

99
        self._update_config()
ale's avatar
ale committed
100 101 102 103 104 105 106

        # Generate keys if they do not exist.
        if not os.path.exists(self.files.public_key):
            tmpdir = tempfile.mkdtemp()
            csr_file = os.path.join(tmpdir, 'ca.csr')
            log.info('creating new RSA CA CSR')
            openssl_wrap.run_with_config(
ale's avatar
ale committed
107 108
                self.basedir, self.files.conf,
                'req', '-new',
ale's avatar
ale committed
109 110 111 112
                '-passout', 'pass:%s' % self._getpw(),
                '-keyout', self.files.private_key, '-out', csr_file)
            log.info('self-signing RSA CA certificate')
            openssl_wrap.run_with_config(
ale's avatar
ale committed
113 114
                self.basedir, self.files.conf,
                'ca', '-keyfile', self.files.private_key,
ale's avatar
ale committed
115
                '-key', self._getpw(),
ale's avatar
ale committed
116
                '-md', self.config['signature_algorithm'],
ale's avatar
ale committed
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
                '-extensions', 'v3_ca', '-out', self.files.public_key,
                '-days', self.config.get('days', self.config['default_days']),
                '-selfsign', '-infiles', csr_file)
            shutil.rmtree(tmpdir)

        os.umask(old_umask)

        # Make some files public.
        for path in (os.path.join(self.basedir, 'public'),
                     os.path.join(self.basedir, 'public/certs'),
                     self.files.public_key):
            if os.path.isdir(path):
                os.chmod(path, 0755)
            else:
                os.chmod(path, 0644)

    def gencrl(self):
        log.info('generating CRL')
135 136 137

        self._update_config()

138 139
        # Write the CRL in PEM format to a temporary file.
        tmpf = self.files.crl + '.tmp'
ale's avatar
ale committed
140
        openssl_wrap.run_with_config(
ale's avatar
ale committed
141
            self.basedir, self.files.conf,
142
            'ca', '-gencrl', '-out', tmpf,
ale's avatar
ale committed
143
            '-key', self._getpw())
144 145 146 147 148
        # Convert to DER format for distribution.
        openssl_wrap.run(
            'crl', '-inform', 'PEM', '-outform', 'DER',
            '-in', tmpf, '-out', self.files.crl)
        os.remove(tmpf)
ale's avatar
ale committed
149 150 151 152 153
        os.chmod(self.files.crl, 0644)

    def revoke(self, cert):
        log.info('revoking certificate %s', cert.name)
        openssl_wrap.run_with_config(
ale's avatar
ale committed
154 155
            self.basedir, self.files.conf,
            'ca', '-revoke', cert.public_key_file,
ale's avatar
ale committed
156 157 158 159
            '-key', self._getpw())
        self.gencrl()

    def generate(self, cert):
160 161
        self._update_config()

ale's avatar
ale committed
162 163
        expiry = cert.get_expiration_date()
        if expiry and expiry > time.time():
164 165 166 167
            log.warn('certificate is still valid')

        if cert.exists():
            log.warn('revoking previous version')
ale's avatar
ale committed
168 169 170 171 172 173 174 175
            self.revoke(cert)

        log.info('generating new certificate %s', cert.name)
        tmpdir = tempfile.mkdtemp()
        try:
            csr_file = os.path.join(tmpdir, '%s.csr' % cert.name)
            conf_file = os.path.join(tmpdir, '%s.conf' % cert.name)
            ext_file = os.path.join(tmpdir, '%s-ext.conf' % cert.name)
ale's avatar
ale committed
176
            conf = {'usage': 'client, server'}
ale's avatar
ale committed
177 178 179 180 181 182 183 184 185 186 187
            conf.update(self.config)
            conf['cn'] = cert.cn
            conf['days'] = cert.days or self.config['default_days']
            if cert.ou:
                conf['ou'] = cert.ou
            conf['alt_names'] = ''.join(
                ['DNS.%d=%s\n' % (idx + 1, x) 
                 for idx, x in enumerate(cert.alt_names)])
            utils.render(conf_file, 'openssl_config', conf)
            utils.render(ext_file, 'ext_config', conf)
            openssl_wrap.run_with_config(
ale's avatar
ale committed
188 189
                self.basedir, conf_file,
                'req', '-new', '-keyout', cert.private_key_file,
ale's avatar
ale committed
190 191 192
                '-nodes', '-out', csr_file)
            os.chmod(cert.private_key_file, 0600)
            openssl_wrap.run_with_config(
ale's avatar
ale committed
193 194
                self.basedir, conf_file,
                'ca', '-days', conf['days'],
ale's avatar
ale committed
195
                '-key', self._getpw(),
ale's avatar
ale committed
196
                '-md', self.config['signature_algorithm'],
ale's avatar
ale committed
197 198 199 200 201
                '-policy', 'policy_anything', '-out', cert.public_key_file,
                '-extfile', ext_file, '-infiles', csr_file)
        finally:
            shutil.rmtree(tmpdir)