Add directives for ssl certificates
authorJan Dittberner <jandd@cacert.org>
Fri, 6 May 2016 15:17:08 +0000 (17:17 +0200)
committerJan Dittberner <jandd@cacert.org>
Fri, 6 May 2016 15:17:08 +0000 (17:17 +0200)
This commit adds a new extension cacert with implementations of two new
directives: sslcert to define a SSL certificate in place where it is
used and sslcertlist to automatically generate an alphabetically sorted
list of certificates.

The certlist.rst has been modified to use the sslcertlist directive,
while the systems/blog.rst and systems/board.rst have been modified to
use the sslcert directives for defining the certificates.

Note: The extension is far from ready and does not support some common
cases (same certificates on multiple nodes, indexing, backlinks from
certificates to certificate list).

docs/certlist.rst
docs/conf.py
docs/sphinxext/__init__.py [new file with mode: 0644]
docs/sphinxext/cacert.py [new file with mode: 0644]
docs/systems/blog.rst
docs/systems/board.rst

index 754be42..44651c3 100644 (file)
@@ -2,51 +2,4 @@
 X.509 Certificates
 ==================
 
-.. _cert_blog_cacert_org:
-
-blog.cacert.org
-===============
-
-.. index::
-   ! single: Certificate; Blog
-
-+------------------+------------------------------------------------------------------------+
-| Common Name      | blog.cacert.org                                                        |
-+------------------+------------------------------------------------------------------------+
-| Subject Altnames | none                                                                   |
-+------------------+------------------------------------------------------------------------+
-| Key kept at      | :doc:`blog <systems/blog>`:file:`/etc/ssl/private/blog.cacert.org.key` |
-+------------------+------------------------------------------------------------------------+
-| Cert kept at     | :doc:`blog <systems/blog>`:file:`/etc/ssl/public/blog.cacert.org.crt`  |
-+------------------+------------------------------------------------------------------------+
-| Serial Number    | 1173559 (0x11e837)                                                     |
-+------------------+------------------------------------------------------------------------+
-| Expiration date  | Mar 31 16:34:28 2018 GMT                                               |
-+------------------+------------------------------------------------------------------------+
-| SHA1 Fingerprint | ``69:A5:5F:3E:1B:D8:2E:CB:B3:AB:0B:E9:81:A6:CF:31:DF:C8:A4:5F``        |
-+------------------+------------------------------------------------------------------------+
-
-.. _cert_board_cacert_org:
-
-board.cacert.org
-================
-
-.. index::
-   ! single: Certificate; Board
-
-+------------------+--------------------------------------------------------------------+
-| Common Name      | board.cacert.org                                                   |
-+==================+====================================================================+
-| Subject Altnames | none                                                               |
-+------------------+--------------------------------------------------------------------+
-| Key kept at      | :doc:`board <systems/board>`:file:`/etc/ssl/private/board.key.pem` |
-+------------------+--------------------------------------------------------------------+
-| Cert kept at     | :doc:`board <systems/board>`:file:`/etc/ssl/certs/board.crt`       |
-+------------------+--------------------------------------------------------------------+
-| Serial Number    | 1173561 (0x11e839)                                                 |
-+------------------+--------------------------------------------------------------------+
-| Expiration date  | Mar 31 16:47:11 2018 GMT                                           |
-+------------------+--------------------------------------------------------------------+
-| SHA1 Fingerprint | ``2C:AC:8C:F8:D6:4A:9E:1D:B0:35:B8:E4:5E:24:B1:43:E3:69:98:46``    |
-+------------------+--------------------------------------------------------------------+
-
+.. sslcertlist::
index c2a57d5..d612007 100644 (file)
@@ -35,6 +35,7 @@ extensions = [
     'sphinx.ext.extlinks',
     'jandd.sphinxext.ip',
     'jandd.sphinxext.mac',
+    'sphinxext.cacert',
 ]
 
 # Add any paths that contain templates here, relative to this directory.
diff --git a/docs/sphinxext/__init__.py b/docs/sphinxext/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs/sphinxext/cacert.py b/docs/sphinxext/cacert.py
new file mode 100644 (file)
index 0000000..12da1d3
--- /dev/null
@@ -0,0 +1,315 @@
+# -*- python -*-
+# This module provides the following CAcert specific sphinx directives
+#
+# sslcert
+# sslcertlist
+# sshkeys
+# sshkeylist
+
+__version__ = '0.1.0'
+
+import re
+
+from docutils import nodes
+from docutils.parsers.rst import Directive
+from docutils.parsers.rst import directives
+from docutils.parsers.rst import roles
+
+from sphinx.util.nodes import set_source_info
+
+
+class sslcert_node(nodes.General, nodes.Element):
+    pass
+
+
+class sslcertlist_node(nodes.General, nodes.Element):
+    pass
+
+
+def hex_int(argument):
+    value = int(argument, base=16)
+    return value
+
+
+def sha1_fingerprint(argument):
+    value = argument.strip().lower()
+    if not re.match(r'^([0-9a-f]{2}:){19}[0-9a-f]{2}$', value):
+        raise ValueError('no correctly formatted SHA1 fingerprint')
+    return value
+
+
+def create_table_row(rowdata):
+    row = nodes.row()
+    for cell in rowdata:
+        entry = nodes.entry()
+        row += entry
+        entry += cell
+    return row
+
+
+def subject_alternative_names(argument):
+    import pdb; pdb.set_trace()
+    value = [san.strip() for san in argument.strip()]
+    # TODO: sanity checks for SANs
+    return value
+
+
+def expiration_date(argument):
+    # TODO: normalize to internal format
+    return directives.unchanged_required(argument)
+
+
+class CAcertSSLCert(Directive):
+    """
+    The sslcert directive implementation.
+
+    There must only be one instance of a certificate with the same CN and
+    serial number that is not flagged as secondary
+    """
+    final_argument_whitespace = True
+    required_arguments = 1
+    option_spec = {
+        'certfile': directives.path,
+        'keyfile': directives.path,
+        'serial': hex_int,
+        'expiration': expiration_date,
+        'sha1fp': sha1_fingerprint,
+        'altnames': subject_alternative_names,
+        'issuer': directives.unchanged_required,
+        'secondary': directives.flag
+    }
+
+    def run(self):
+        if self.options.get('secondary'):
+            missing = [
+                required for required in ('certfile', 'keyfile', 'serial')
+                if required not in self.options
+            ]
+        else:
+            missing = [
+                required for required in (
+                    'certfile', 'keyfile', 'serial', 'expiration', 'sha1fp',
+                    'issuer')
+                if required not in self.options
+            ]
+        if missing:
+            raise self.error(
+                "required option(s) '%s' is/are not set for %s." % (
+                    "', '".join(missing), self.name))
+        sslcert = sslcert_node()
+        sslcert.attributes['certdata'] = self.options.copy()
+        sslcert.attributes['certdata']['cn'] = self.arguments[0]
+        set_source_info(self, sslcert)
+
+        env = self.state.document.settings.env
+        targetid = 'sslcert-%s' % env.new_serialno('sslcert')
+        targetnode = nodes.target('', '', ids=[targetid])
+        para = nodes.paragraph()
+        para.append(targetnode)
+        para.append(sslcert)
+        return [para]
+
+class CAcertSSLCertList(Directive):
+    """
+    The sslcertlist directive implementation
+    """
+    def run(self):
+        return [sslcertlist_node()]
+
+class CAcertSSHKeys(Directive):
+    """
+    The sshkeys directive implementation that can be used to specify the ssh
+    host keys for a host.
+    """
+    def run(self):
+        return []
+
+class CAcertSSHKeyList(Directive):
+    """
+    The sshkeylist directive implementation
+    """
+    def run(self):
+        return []
+
+
+def _create_interpreted_file_node(text, line=0):
+    return roles._roles['file']('', ':file:`%s`' % text,
+                                text, line, None)[0][0]
+
+
+def process_sslcerts(app, doctree):
+    env = app.builder.env
+    if not hasattr(env, 'cacert_sslcerts'):
+        env.cacert_sslcerts = []
+    for node in doctree.traverse(sslcert_node):
+        try:
+            targetnode = node.parent[node.parent.index(node) - 1]
+            if not isinstance(targetnode, nodes.target):
+                raise IndexError
+        except IndexError:
+            targetnode = None
+        newnode = node.deepcopy()
+        del newnode['ids']
+        certdata = node.attributes['certdata']
+        env.cacert_sslcerts.append({
+            'docname': env.docname,
+            'source': node.source or env.doc2path(env.docname),
+            'lineno': node.line,
+            'sslcert': certdata,
+            'target': targetnode,
+        })
+
+        bullets = nodes.bullet_list()
+        certitem = nodes.list_item()
+        bullets += certitem
+        certpara = nodes.paragraph()
+        certpara += nodes.Text('Certificate for CN %s' % certdata['cn'])
+        certitem += certpara
+
+        subbullets = nodes.bullet_list()
+        bullets += subbullets
+        item = nodes.list_item()
+        subbullets += item
+        certfile = nodes.paragraph(text="certificate in file ")
+        certfile += _create_interpreted_file_node(
+            certdata['certfile'], node.line)
+        item += certfile
+        item = nodes.list_item()
+        subbullets += item
+        keyfile = nodes.paragraph(text="private key in file ")
+        keyfile += _create_interpreted_file_node(
+            certdata['keyfile'], node.line)
+        item += keyfile
+
+        # TODO add reference to item in certificate list
+        # TODO add index entry
+
+        node.parent.replace_self([targetnode, bullets])
+
+
+def _sslcert_item_key(item):
+    return item['sslcert']['cn']
+
+
+def _build_cert_anchor_name(cn, serial):
+    return 'cert_%s_%d' % (cn.replace('.', '_'), serial)
+
+
+def _format_subject_alternative_names(altnames):
+    return nodes.paragraph(text = ", ".join(altnames))
+
+
+def _file_ref_paragraph(cert_info, filekey, app, env, docname):
+    para = nodes.paragraph()
+    title = env.titles[cert_info['docname']].astext().lower()
+    refnode = nodes.reference('', '', internal=True)
+    # TODO add support for multiple key/certificate locations
+    refnode['refuri'] = app.builder.get_relative_uri(
+        docname, cert_info['docname']) + '#' + cert_info['target']['ids'][0]
+    refnode += nodes.Text(title)
+    para += refnode
+    para += nodes.Text(":")
+    para += _create_interpreted_file_node(cert_info['sslcert'][filekey])
+    return para
+
+
+def _format_serial_number(serial):
+    return nodes.paragraph(text="%d (0x%0x)" % (serial, serial))
+
+
+def _format_expiration_date(expiration):
+    # TODO use a normalized date format
+    return nodes.paragraph(text=expiration)
+
+
+def _format_fingerprint(fingerprint):
+    para = nodes.paragraph()
+    para += nodes.literal(text=fingerprint, classes=['fingerprint'])
+    return para
+
+
+def process_sslcert_nodes(app, doctree, docname):
+    env = app.builder.env
+
+    if not hasattr(env, 'cacert_sslcerts'):
+        env.cacert_all_certs = []
+
+    for node in doctree.traverse(sslcertlist_node):
+        content = []
+
+        for cert_info in sorted(env.cacert_sslcerts, key=_sslcert_item_key):
+            cert_sec = nodes.section()
+            cert_sec['ids'].append(
+                _build_cert_anchor_name(cert_info['sslcert']['cn'],
+                                        cert_info['sslcert']['serial'])
+            )
+            cert_sec += nodes.title(text=cert_info['sslcert']['cn'])
+            table = nodes.table()
+            cert_sec += table
+            tgroup = nodes.tgroup(cols=2)
+            table += tgroup
+            tgroup += nodes.colspec(colwidth=1)
+            tgroup += nodes.colspec(colwidth=3)
+            tbody = nodes.tbody()
+            tgroup += tbody
+            tbody += create_table_row([
+                nodes.paragraph(text='Common Name'),
+                nodes.paragraph(text=cert_info['sslcert']['cn'])
+            ])
+            if 'altnames' in cert_info['sslcert']:
+                tbody += create_table_row([
+                    nodes.paragraph(text='Subject Alternative Names'),
+                    _format_subject_alternative_names(
+                        cert_info['sslcert']['altnames'])
+                ])
+            tbody += create_table_row([
+                nodes.paragraph(text='Key kept at'),
+                _file_ref_paragraph(cert_info, 'keyfile', app, env, docname)
+            ])
+            tbody += create_table_row([
+                nodes.paragraph(text='Cert kept at'),
+                _file_ref_paragraph(cert_info, 'certfile', app, env, docname)
+            ])
+            tbody += create_table_row([
+                nodes.paragraph(text='Serial number'),
+                _format_serial_number(cert_info['sslcert']['serial'])
+            ])
+            tbody += create_table_row([
+                nodes.paragraph(text='Expiration date'),
+                _format_expiration_date(cert_info['sslcert']['expiration'])
+            ])
+            tbody += create_table_row([
+                nodes.paragraph(text='Issuer'),
+                nodes.paragraph(text=cert_info['sslcert']['issuer'])
+            ])
+            tbody += create_table_row([
+                nodes.paragraph(text='SHA1 fingerprint'),
+                _format_fingerprint(cert_info['sslcert']['sha1fp'])
+            ])
+            content.append(cert_sec)
+
+        node.replace_self(content)
+
+
+def merge_info(env, docnames, other):
+    pass
+
+
+def purge_sslcerts(app, env, docname):
+    pass
+
+
+def setup(app):
+    app.add_node(sslcertlist_node)
+    app.add_node(sslcert_node)
+
+    app.add_directive('sslcert', CAcertSSLCert)
+    app.add_directive('sslcertlist', CAcertSSLCertList)
+    app.add_directive('sshkeys', CAcertSSHKeys)
+    app.add_directive('sshkeylist', CAcertSSHKeyList)
+
+    app.connect('doctree-read', process_sslcerts)
+    app.connect('doctree-resolved', process_sslcert_nodes)
+    app.connect('env-purge-doc', purge_sslcerts)
+    app.connect('env-merge-info', merge_info)
+    return {'version': __version__}
index 38bb50d..e216a85 100644 (file)
@@ -280,11 +280,14 @@ Critical Configuration items
 Keys and X.509 certificates
 ---------------------------
 
-.. index::
-   single: Certificate; Blog
+.. sslcert:: blog.cacert.org
+   :certfile:   /etc/ssl/public/blog.cacert.org.crt
+   :keyfile:    /etc/ssl/private/blog.cacert.org.key
+   :serial:     11e837
+   :expiration: Mar 31 16:34:28 2018 GMT
+   :sha1fp:     69:A5:5F:3E:1B:D8:2E:CB:B3:AB:0B:E9:81:A6:CF:31:DF:C8:A4:5F
+   :issuer:     CAcert.org Class 1 Root CA
 
-* :file:`/etc/ssl/public/blog.cacert.org.crt` server certificate
-* :file:`/etc/ssl/private/blog.cacert.org.key` server key
 * :file:`/etc/ssl/certs/cacert.org/` directory containing CAcert.org Class 1
   and Class 3 certificates (allowed CA certificates for client certificates)
   and symlinks with hashed names as expected by OpenSSL
index c400935..b454b27 100644 (file)
@@ -293,17 +293,19 @@ Critical Configuration items
 Keys and X.509 certificates
 ---------------------------
 
-.. index::
-   single: Certificate; Board
+.. sslcert:: board.cacert.org
+   :certfile:   /etc/ssl/certs/board.crt
+   :keyfile:    /etc/ssl/private/board.key
+   :serial:     11e839
+   :expiration: Mar 31 16:47:11 2018 GMT
+   :sha1fp:     2C:AC:8C:F8:D6:4A:9E:1D:B0:35:B8:E4:5E:24:B1:43:E3:69:98:46
+   :issuer:     CAcert.org Class 1 Root CA
 
-* :file:`/etc/ssl/certs/board.crt` server certificate
-* :file:`/etc/ssl/private/board.key` server key
 * :file:`/etc/ssl/certs/cacert.org.pem` CAcert.org Class 1 and Class 3 CA
   certificates (allowed CA certificates for client certificates)
 
 .. seealso::
 
-   * :ref:`cert_board_cacert_org` in :doc:`../certlist`
    * :wiki:`SystemAdministration/CertificateList`
 
 Apache configuration files