summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorJan Dittberner <jandd@cacert.org>2016-05-07 22:42:48 +0200
committerJan Dittberner <jandd@cacert.org>2016-05-07 22:42:48 +0200
commitacfd810d571074a10f3b5f84cf2b386deae566b4 (patch)
treef8c3a79c59e1ce29f379331d15b108025ccf9298 /docs
parent031d5e8f85472348b985edf2b59f4b741e56a928 (diff)
downloadcacert-infradocs-acfd810d571074a10f3b5f84cf2b386deae566b4.tar.gz
cacert-infradocs-acfd810d571074a10f3b5f84cf2b386deae566b4.tar.xz
cacert-infradocs-acfd810d571074a10f3b5f84cf2b386deae566b4.zip
Add SSH host key list generation
This commit adds implementations for the directives sshkeys and sshkeylist that replace manually written SSH key lists with automatically generated ones.
Diffstat (limited to 'docs')
-rw-r--r--docs/sphinxext/cacert.py237
1 files changed, 229 insertions, 8 deletions
diff --git a/docs/sphinxext/cacert.py b/docs/sphinxext/cacert.py
index 375bfe7..aa728f3 100644
--- a/docs/sphinxext/cacert.py
+++ b/docs/sphinxext/cacert.py
@@ -7,6 +7,7 @@
# sshkeylist
import re
+import os.path
from ipaddress import ip_address
from docutils import nodes
@@ -16,13 +17,15 @@ from docutils.parsers.rst import roles
from sphinx import addnodes
from sphinx.errors import SphinxError
-from sphinx.util.nodes import set_source_info, make_refnode
+from sphinx.util.nodes import set_source_info, make_refnode, traverse_parent
from dateutil.parser import parse as date_parse
from validate_email import validate_email
__version__ = '0.1.0'
+SUPPORTED_SSH_KEYTYPES = ('RSA', 'DSA', 'ECDSA', 'ED25519')
+
class sslcert_node(nodes.General, nodes.Element):
pass
@@ -32,6 +35,14 @@ class sslcertlist_node(nodes.General, nodes.Element):
pass
+class sshkeys_node(nodes.General, nodes.Element):
+ pass
+
+
+class sshkeylist_node(nodes.General, nodes.Element):
+ pass
+
+
# mapping and validation functions for directive options
def hex_int(argument):
@@ -39,6 +50,13 @@ def hex_int(argument):
return value
+def md5_fingerprint(argument):
+ value = argument.strip().lower()
+ if not re.match(r'^([0-9a-f]{2}:){15}[0-9a-f]{2}$', value):
+ raise ValueError('no correctly formatted SHA1 fingerprint')
+ 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):
@@ -150,8 +168,26 @@ class CAcertSSHKeys(Directive):
The sshkeys directive implementation that can be used to specify the ssh
host keys for a host.
"""
+ option_spec = {
+ keytype.lower(): md5_fingerprint for keytype in SUPPORTED_SSH_KEYTYPES
+ }
def run(self):
- return []
+ if len(self.options) == 0:
+ raise self.error(
+ "at least one ssh key fingerprint must be specified. The "
+ "following formats are supported: %s" % ", ".join(
+ SUPPORTED_SSH_KEYTYPES))
+ sshkeys = sshkeys_node()
+ sshkeys.attributes['keys'] = self.options.copy()
+ set_source_info(self, sshkeys)
+
+ env = self.state.document.settings.env
+ secid = 'sshkeys-%s' % env.new_serialno('sshkeys')
+
+ section = nodes.section(ids=[secid])
+ section += nodes.title(text='SSH host keys')
+ section += sshkeys
+ return [section]
class CAcertSSHKeyList(Directive):
@@ -159,7 +195,7 @@ class CAcertSSHKeyList(Directive):
The sshkeylist directive implementation
"""
def run(self):
- return []
+ return [sshkeylist_node()]
def create_table_row(rowdata):
@@ -180,6 +216,10 @@ def _sslcert_item_key(item):
return "%s-%d" % (item['cn'], item['serial'])
+def _sshkeys_item_key(item):
+ return "%s" % os.path.basename(item['docname'])
+
+
def _build_cert_anchor_name(cn, serial):
return 'cert_%s_%d' % (cn.replace('.', '_'), serial)
@@ -237,6 +277,19 @@ def _get_cert_index_text(cert_info):
return "Certificate; %s" % cert_info['cn']
+def _get_formatted_keyentry(keys_info, algorithm):
+ entry = nodes.entry()
+ algkey = algorithm.lower()
+ if algkey in keys_info:
+ para = nodes.paragraph()
+ keyfp = nodes.literal(text=keys_info[algkey])
+ para += keyfp
+ else:
+ para = nodes.paragraph(text="-")
+ entry += para
+ return entry
+
+
def process_sslcerts(app, doctree):
env = app.builder.env
if not hasattr(env, 'cacert_sslcerts'):
@@ -326,6 +379,79 @@ def process_sslcerts(app, doctree):
env.note_indexentries_from(env.docname, doctree)
+def process_sshkeys(app, doctree):
+ env = app.builder.env
+ if not hasattr(env, 'cacert_sshkeys'):
+ env.cacert_sshkeys = []
+
+ for node in doctree.traverse(sshkeylist_node):
+ if hasattr(env, 'cacert_sshkeylistdoc'):
+ raise SphinxError(
+ "There must be one sshkeylist directive present in "
+ "the document tree only.")
+ env.cacert_sshkeylistdoc = env.docname
+
+ for node in doctree.traverse(sshkeys_node):
+ # find section
+ section = [s for s in traverse_parent(node, nodes.section)][0]
+ dockeys = {'docname': env.docname, 'secid': section['ids'][0]}
+ dockeys.update(node['keys'])
+ env.cacert_sshkeys.append(dockeys)
+
+ secparent = section.parent
+ pos = secparent.index(section)
+ # add index node for section
+ indextitle = 'SSH host key; %s' % (
+ env.docname in env.titles and env.titles[env.docname].astext()
+ or os.path.basename(env.docname)
+ )
+ secparent.insert(pos, addnodes.index(entries=[
+ ('pair', indextitle, section['ids'][0], '', None),
+ ]))
+
+ # add table
+ content = []
+ table = nodes.table()
+ content.append(table)
+ cols = (1, 4)
+ tgroup = nodes.tgroup(cols=len(cols))
+ table += tgroup
+ for col in cols:
+ tgroup += nodes.colspec(colwidth=col)
+ thead = nodes.thead()
+ tgroup += thead
+ thead += create_table_row([
+ nodes.paragraph(text='Algorithm'),
+ nodes.paragraph(text='Fingerprint'),
+ ])
+ tbody = nodes.tbody()
+ tgroup += tbody
+ for alg in SUPPORTED_SSH_KEYTYPES:
+ if alg.lower() in dockeys:
+ fpparagraph = nodes.paragraph()
+ fpparagraph += nodes.literal(text=dockeys[alg.lower()])
+ else:
+ fpparagraph = nodes.paragraph(text='-')
+ tbody += create_table_row([
+ nodes.paragraph(text=alg),
+ fpparagraph,
+ ])
+ # add pending_xref for link to ssh key list
+ seealso = addnodes.seealso()
+ content.append(seealso)
+ detailref = addnodes.pending_xref(
+ reftype='sshkeyref', refdoc=env.docname, refid='sshkeylist',
+ reftarget='sshkeylist'
+ )
+ detailref += nodes.Text("SSH host key list")
+ seepara = nodes.paragraph()
+ seepara += detailref
+ seealso += seepara
+
+ node.replace_self(content)
+ env.note_indexentries_from(env.docname, doctree)
+
+
def process_sslcert_nodes(app, doctree, docname):
env = app.builder.env
@@ -405,13 +531,90 @@ def process_sslcert_nodes(app, doctree, docname):
env.note_indexentries_from(docname, doctree)
+def process_sshkeys_nodes(app, doctree, docname):
+ env = app.builder.env
+
+ if not hasattr(env, 'cacert_sshkeys'):
+ env.cacert_sslcerts = []
+
+ for node in doctree.traverse(sshkeylist_node):
+ content = []
+ content.append(nodes.target(ids=['sshkeylist']))
+
+ if len(env.cacert_sshkeys) > 0:
+ table = nodes.table()
+ content.append(table)
+ tgroup = nodes.tgroup(cols=3)
+ tgroup += nodes.colspec(colwidth=1)
+ tgroup += nodes.colspec(colwidth=1)
+ tgroup += nodes.colspec(colwidth=4)
+ table += tgroup
+
+ thead = nodes.thead()
+ row = nodes.row()
+ entry = nodes.entry()
+ entry += nodes.paragraph(text="Host")
+ row += entry
+ entry = nodes.entry(morecols=1)
+ entry += nodes.paragraph(text="SSH Host Keys")
+ row += entry
+ thead += row
+ tgroup += thead
+
+ tbody = nodes.tbody()
+ tgroup += tbody
+
+ for keys_info in sorted(env.cacert_sshkeys, key=_sshkeys_item_key):
+ trow = nodes.row()
+ entry = nodes.entry(morerows=len(SUPPORTED_SSH_KEYTYPES) - 1)
+ para = nodes.paragraph()
+ para += make_refnode(
+ app.builder, docname, keys_info['docname'],
+ keys_info['secid'],
+ nodes.Text(env.titles[keys_info['docname']].astext())
+ )
+ entry += para
+ trow += entry
+
+ entry = nodes.entry()
+ entry += nodes.paragraph(text=SUPPORTED_SSH_KEYTYPES[0])
+ trow += entry
+
+ trow += _get_formatted_keyentry(
+ keys_info, SUPPORTED_SSH_KEYTYPES[0])
+
+ tbody += trow
+
+ for algorithm in SUPPORTED_SSH_KEYTYPES[1:]:
+ trow = nodes.row()
+
+ entry = nodes.entry()
+ entry += nodes.paragraph(text=algorithm)
+ trow += entry
+
+ trow += _get_formatted_keyentry(keys_info, algorithm)
+ tbody += trow
+ else:
+ content.append(nodes.paragraph(
+ text="No ssh keys have been documented.")
+ )
+
+ node.replace_self(content)
+
+
def resolve_missing_reference(app, env, node, contnode):
- if not hasattr(env, 'cacert_certlistdoc'):
- return
if node['reftype'] == 'certlistref':
- return make_refnode(
- app.builder, node['refdoc'], env.cacert_certlistdoc,
- node['refid'], contnode)
+ if hasattr(env, 'cacert_certlistdoc'):
+ return make_refnode(
+ app.builder, node['refdoc'], env.cacert_certlistdoc,
+ node['refid'], contnode)
+ raise SphinxError('No certlist directive found in the document tree')
+ if node['reftype'] == 'sshkeyref' :
+ if hasattr(env, 'cacert_sshkeylistdoc'):
+ return make_refnode(
+ app.builder, node['refdoc'], env.cacert_sshkeylistdoc,
+ node['refid'], contnode)
+ raise SphinxError('No sshkeylist directive found in the document tree')
def purge_sslcerts(app, env, docname):
@@ -429,9 +632,24 @@ def purge_sslcerts(app, env, docname):
]
+def purge_sshkeys(app, env, docname):
+ if (
+ hasattr(env, 'cacert_sshkeylistdoc') and
+ env.cacert_sshkeylistdoc == docname
+ ):
+ delattr(env, 'cacert_sshkeylistdoc')
+ if not hasattr(env, 'cacert_sshkeys'):
+ return
+ env.cacert_sshkeys = [
+ keys for keys in env.cacert_sshkeys if keys['docname'] != docname
+ ]
+
+
def setup(app):
app.add_node(sslcertlist_node)
app.add_node(sslcert_node)
+ app.add_node(sshkeylist_node)
+ app.add_node(sshkeys_node)
app.add_directive('sslcert', CAcertSSLCert)
app.add_directive('sslcertlist', CAcertSSLCertList)
@@ -439,7 +657,10 @@ def setup(app):
app.add_directive('sshkeylist', CAcertSSHKeyList)
app.connect('doctree-read', process_sslcerts)
+ app.connect('doctree-read', process_sshkeys)
app.connect('doctree-resolved', process_sslcert_nodes)
+ app.connect('doctree-resolved', process_sshkeys_nodes)
app.connect('missing-reference', resolve_missing_reference)
app.connect('env-purge-doc', purge_sslcerts)
+ app.connect('env-purge-doc', purge_sshkeys)
return {'version': __version__}