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)