Parcourir la source

[master] Merge branch 'trac1044'

Jelte Jansen il y a 12 ans
Parent
commit
43b0ea4216

+ 0 - 1
configure.ac

@@ -705,7 +705,6 @@ fi
 AC_SUBST(BOTAN_LDFLAGS)
 AC_SUBST(BOTAN_LIBS)
 AC_SUBST(BOTAN_INCLUDES)
-
 # Even though chances are high we already performed a real compilation check
 # in the search for the right (pkg)config data, we try again here, to
 # be sure.

+ 2 - 0
src/bin/cmdctl/.gitignore

@@ -4,3 +4,5 @@
 /cmdctl.spec.pre
 /run_b10-cmdctl.sh
 /b10-cmdctl.8
+/cmdctl-keyfile.pem
+/cmdctl-certfile.pem

+ 20 - 4
src/bin/cmdctl/Makefile.am

@@ -4,6 +4,8 @@ pkglibexecdir = $(libexecdir)/@PACKAGE@
 
 pkglibexec_SCRIPTS = b10-cmdctl
 
+bin_PROGRAMS = b10-certgen
+
 nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/cmdctl_messages.py
 pylogmessagedir = $(pyexecdir)/isc/log_messages/
 
@@ -25,15 +27,18 @@ CLEANFILES= b10-cmdctl cmdctl.pyc cmdctl.spec
 CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/cmdctl_messages.py
 CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/cmdctl_messages.pyc
 
-man_MANS = b10-cmdctl.8
-DISTCLEANFILES = $(man_MANS)
-EXTRA_DIST += $(man_MANS) b10-cmdctl.xml cmdctl_messages.mes
+man_MANS = b10-cmdctl.8 b10-certgen.1
+DISTCLEANFILES = $(man_MANS) cmdctl-certfile.pem cmdctl-keyfile.pem
+EXTRA_DIST += $(man_MANS) b10-certgen.xml b10-cmdctl.xml cmdctl_messages.mes
 
 if GENERATE_DOCS
 
 b10-cmdctl.8: b10-cmdctl.xml
 	@XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-cmdctl.xml
 
+b10-certgen.1: b10-certgen.xml
+	@XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-certgen.xml
+
 else
 
 $(man_MANS):
@@ -54,12 +59,23 @@ b10-cmdctl: cmdctl.py $(PYTHON_LOGMSGPKG_DIR)/work/cmdctl_messages.py
 	$(SED) "s|@@PYTHONPATH@@|@pyexecdir@|" cmdctl.py >$@
 	chmod a+x $@
 
+b10_certgen_SOURCES = b10-certgen.cc
+b10_certgen_CXXFLAGS = $(BOTAN_INCLUDES)
+b10_certgen_LDFLAGS = $(BOTAN_LIBS)
+
+# Generate the initial certificates immediately
+cmdctl-certfile.pem: b10-certgen
+	./b10-certgen -q -w
+
+cmdctl-keyfile.pem: b10-certgen
+	./b10-certgen -q -w
+
 if INSTALL_CONFIGURATIONS
 
 # Below we intentionally use ${INSTALL} -m 640 instead of $(INSTALL_DATA)
 # because these file will contain sensitive information.
 install-data-local:
-	$(mkinstalldirs) $(DESTDIR)/@sysconfdir@/@PACKAGE@   
+	$(mkinstalldirs) $(DESTDIR)/@sysconfdir@/@PACKAGE@
 	for f in $(CMDCTL_CONFIGURATIONS) ; do	\
 	  if test ! -f $(DESTDIR)$(sysconfdir)/@PACKAGE@/$$f; then	\
 	    ${INSTALL} -m 640 $(srcdir)/$$f $(DESTDIR)$(sysconfdir)/@PACKAGE@/ ;	\

+ 426 - 0
src/bin/cmdctl/b10-certgen.cc

@@ -0,0 +1,426 @@
+// Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <botan/botan.h>
+#include <botan/x509self.h>
+#include <botan/x509stor.h>
+#include <botan/rsa.h>
+#include <botan/dsa.h>
+#include <botan/data_src.h>
+using namespace Botan;
+
+#include <iostream>
+#include <fstream>
+#include <memory>
+#include <getopt.h>
+
+// For cleaner 'does not exist or is not readable' output than
+// botan provides
+#include <unistd.h>
+#include <errno.h>
+
+// This is a simple tool that creates a self-signed PEM certificate
+// for use with BIND 10. It creates a simple certificate for initial
+// setup. Currently, all values are hardcoded defaults. For future
+// versions, we may want to add more options for administrators.
+
+// It will create a PEM file containing a certificate with the following
+// values:
+// common name: localhost
+// organization: BIND10
+// country code: US
+
+// Additional error return codes; these are specifically
+// chosen to be distinct from validation error codes as
+// provided by Botan. Their main use is to distinguish
+// error cases in the unit tests.
+const int DECODING_ERROR = 100;
+const int BAD_OPTIONS = 101;
+const int READ_ERROR = 102;
+const int WRITE_ERROR = 103;
+const int UNKNOWN_ERROR = 104;
+const int NO_SUCH_FILE = 105;
+const int FILE_PERMISSION_ERROR = 106;
+
+void
+usage() {
+    std::cout << "Usage: b10-certgen [OPTION]..." << std::endl;
+    std::cout << "Validate, create, or update a self-signed certificate for "
+                 "use with b10-cmdctl" << std::endl;
+    std::cout << "" << std::endl;
+    std::cout << "Options:" << std::endl;
+    std::cout << "-c, --certfile=FILE\t\tfile to read or store the certificate"
+              << std::endl;
+    std::cout << "-f, --force\t\t\toverwrite existing certficate even if it"
+              << std::endl <<"\t\t\t\tis valid" << std::endl;
+    std::cout << "-h, --help\t\t\tshow this help" << std::endl;
+    std::cout << "-k, --keyfile=FILE\t\tfile to store the generated private key"
+              << std::endl;
+    std::cout << "-w, --write\t\t\tcreate a new certificate if the given file"
+              << std::endl << "\t\t\t\tdoes not exist, or if is is not valid"
+              << std::endl;
+    std::cout << "-q, --quiet\t\t\tprint no output when creating or validating"
+              << std::endl;
+}
+
+/// \brief Returns true if the given file exists
+///
+/// \param filename The file to check
+/// \return true if file exists
+bool
+fileExists(const std::string& filename) {
+    return (access(filename.c_str(), F_OK) == 0);
+}
+
+/// \brief Returns true if the given file exists and is readable
+///
+/// \param filename The file to check
+/// \return true if file exists and is readable
+bool
+fileIsReadable(const std::string& filename) {
+    return (access(filename.c_str(), R_OK) == 0);
+}
+
+/// \brief Returns true if the given file exists and is writable
+///
+/// \param filename The file to check
+/// \return true if file exists and is writable
+bool
+fileIsWritable(const std::string& filename) {
+    return (access(filename.c_str(), W_OK) == 0);
+}
+
+/// \brief Helper function for readable error output;
+///
+/// Returns string representation of X509 result code
+/// This does not appear to be provided by Botan itself
+///
+/// \param code An \c X509_Code instance
+/// \return A human-readable c string
+const char*
+X509CodeToString(const X509_Code& code) {
+    // note that this list provides more than we would
+    // need in this context, it is just the enum from
+    // the source code of Botan.
+    switch (code) {
+    case VERIFIED:
+        return ("verified");
+    case UNKNOWN_X509_ERROR:
+        return ("unknown x509 error");
+    case CANNOT_ESTABLISH_TRUST:
+        return ("cannot establish trust");
+    case CERT_CHAIN_TOO_LONG:
+        return ("certificate chain too long");
+    case SIGNATURE_ERROR:
+        return ("signature error");
+    case POLICY_ERROR:
+        return ("policy error");
+    case INVALID_USAGE:
+        return ("invalid usage");
+    case CERT_FORMAT_ERROR:
+        return ("certificate format error");
+    case CERT_ISSUER_NOT_FOUND:
+        return ("certificate issuer not found");
+    case CERT_NOT_YET_VALID:
+        return ("certificate not yet valid");
+    case CERT_HAS_EXPIRED:
+        return ("certificate has expired");
+    case CERT_IS_REVOKED:
+        return ("certificate has been revoked");
+    case CRL_FORMAT_ERROR:
+        return ("crl format error");
+    case CRL_NOT_YET_VALID:
+        return ("crl not yet valid");
+    case CRL_HAS_EXPIRED:
+        return ("crl has expired");
+    case CA_CERT_CANNOT_SIGN:
+        return ("CA cert cannot sign");
+    case CA_CERT_NOT_FOR_CERT_ISSUER:
+        return ("CA certificate not for certificate issuer");
+    case CA_CERT_NOT_FOR_CRL_ISSUER:
+        return ("CA certificate not for crl issuer");
+    default:
+        return ("Unknown X509 code");
+    }
+}
+
+class CertificateTool {
+public:
+    CertificateTool(bool quiet) : quiet_(quiet) {}
+
+    int
+    createKeyAndCertificate(const std::string& key_file_name,
+                            const std::string& cert_file_name) {
+        try {
+            AutoSeeded_RNG rng;
+
+            // Create and store a private key
+            print("Creating key file " + key_file_name);
+            RSA_PrivateKey key(rng, 2048);
+            std::ofstream key_file(key_file_name.c_str());
+            if (!key_file.good()) {
+                print(std::string("Error writing to ") + key_file_name +
+                      ": " + strerror(errno));
+                return (WRITE_ERROR);
+            }
+            key_file << PKCS8::PEM_encode(key, rng, "");
+            if (!key_file.good()) {
+                print(std::string("Error writing to ") + key_file_name +
+                      ": " + strerror(errno));
+                return (WRITE_ERROR);
+            }
+            key_file.close();
+
+            // Certificate options, currently hardcoded.
+            // For a future version we may want to make these
+            // settable.
+            X509_Cert_Options opts;
+            opts.common_name = "localhost";
+            opts.organization = "UNKNOWN";
+            opts.country = "XX";
+
+            opts.CA_key();
+
+            print("Creating certificate file " + cert_file_name);
+
+            // The exact call changed aftert 1.8, adding the
+            // hash function option
+#if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,9,0)
+            X509_Certificate cert =
+            X509::create_self_signed_cert(opts, key, "SHA-256", rng);
+#else
+            X509_Certificate cert =
+            X509::create_self_signed_cert(opts, key, rng);
+#endif
+
+            std::ofstream cert_file(cert_file_name.c_str());
+            if (!cert_file.good()) {
+                print(std::string("Error writing to ") + cert_file_name +
+                      ": " + strerror(errno));
+                return (WRITE_ERROR);
+            }
+            cert_file << cert.PEM_encode();
+            if (!cert_file.good()) {
+                print(std::string("Error writing to ") + cert_file_name +
+                      ": " + strerror(errno));
+                return (WRITE_ERROR);
+            }
+            cert_file.close();
+        } catch(std::exception& e) {
+            std::cout << "Error creating key or certificate: " << e.what()
+                      << std::endl;
+            return (UNKNOWN_ERROR);
+        }
+        return (0);
+    }
+
+    int
+    validateCertificate(const std::string& certfile) {
+        // Since we are dealing with a self-signed certificate here, we
+        // also use the certificate to check itself; i.e. we add it
+        // as a trusted certificate, then validate the certficate itself.
+        //const X509_Certificate cert(certfile);
+        try {
+            X509_Store store;
+            DataSource_Stream in(certfile);
+            store.add_trusted_certs(in);
+
+            const X509_Code result = store.validate_cert(certfile);
+
+            if (result == VERIFIED) {
+                print(certfile + " is valid");
+            } else {
+                print(certfile + " failed to verify: " +
+                      X509CodeToString(result));
+            }
+            return (result);
+        } catch (const Botan::Decoding_Error& bde) {
+            print(certfile + " failed to verify: " + bde.what());
+            return (DECODING_ERROR);
+        } catch (const Botan::Stream_IO_Error& bsie) {
+            print(certfile + " not read: " + bsie.what());
+            return (READ_ERROR);
+        }
+    }
+
+    /// \brief Runs the tool
+    ///
+    /// \param create_cert  Create certificate if true, validate if false.
+    ///                     Does nothing if certificate exists and is valid.
+    /// \param force_create Create new certificate even if it is valid.
+    /// \param certfile     Certificate file to read to or write from.
+    /// \param keyfile      Key file to write if certificate is created.
+    ///                     Ignored if create_cert is false
+    /// \return zero on success, non-zero on failure
+    int
+    run(bool create_cert, bool force_create, const std::string& certfile,
+        const std::string& keyfile)
+    {
+        if (create_cert) {
+            // Unless force is given, only create it if the current
+            // one is not OK
+
+            // First do some basic permission checks; both files
+            // should either not exist, or be both readable
+            // and writable
+            // The checks are done one by one so all errors can
+            // be enumerated in one go
+            if (fileExists(certfile)) {
+                if (!fileIsReadable(certfile)) {
+                    print(certfile + " not readable: " + strerror(errno));
+                    create_cert = false;
+                }
+                if (!fileIsWritable(certfile)) {
+                    print(certfile + " not writable: " + strerror(errno));
+                    create_cert = false;
+                }
+            }
+            // The key file really only needs write permissions (for
+            // b10-certgen that is)
+            if (fileExists(keyfile)) {
+                if (!fileIsWritable(keyfile)) {
+                    print(keyfile + " not writable: " + strerror(errno));
+                    create_cert = false;
+                }
+            }
+            if (!create_cert) {
+                print("Not creating new certificate, "
+                      "check file permissions");
+                return (FILE_PERMISSION_ERROR);
+            }
+
+            // If we reach this, we know that if they exist, we can both
+            // read and write them, so now it's up to content checking
+            // and/or force_create
+
+            if (force_create || !fileExists(certfile) ||
+                validateCertificate(certfile) != VERIFIED) {
+                return (createKeyAndCertificate(keyfile, certfile));
+            } else {
+                print("Not creating a new certificate (use -f to force)");
+            }
+        } else {
+            if (!fileExists(certfile)) {
+                print(certfile + ": " + strerror(errno));
+                return (NO_SUCH_FILE);
+            }
+            if (!fileIsReadable(certfile)) {
+                print(certfile + " not readable: " + strerror(errno));
+                return (FILE_PERMISSION_ERROR);
+            }
+            int result = validateCertificate(certfile);
+            if (result != 0) {
+                print("Running with -w would overwrite the certificate");
+            }
+            return (result);
+        }
+        return (0);
+    }
+private:
+    /// Prints the message to stdout unless quiet_ is true
+    void print(const std::string& msg) {
+        if (!quiet_) {
+            std::cout << msg << std::endl;
+        }
+    }
+
+    bool quiet_;
+};
+
+int
+main(int argc, char* argv[])
+{
+    Botan::LibraryInitializer init;
+
+    // create or check certificate
+    bool create_cert = false;
+    // force creation even if not necessary
+    bool force_create = false;
+    // don't print any output
+    bool quiet = false;
+
+    // default certificate file
+    std::string certfile("cmdctl-certfile.pem");
+    // default key file
+    std::string keyfile("cmdctl-keyfile.pem");
+
+    // whether or not the above values have been
+    // overridden (used in command line checking)
+    bool certfile_default = true;
+    bool keyfile_default = true;
+
+    struct option long_options[] = {
+        { "certfile", required_argument, 0, 'c' },
+        { "force", no_argument, 0, 'f' },
+        { "help", no_argument, 0, 'h' },
+        { "keyfile", required_argument, 0, 'k' },
+        { "write", no_argument, 0, 'w' },
+        { "quiet", no_argument, 0, 'q' },
+        { 0, 0, 0, 0 }
+    };
+
+    int opt, option_index;
+    while ((opt = getopt_long(argc, argv, "c:fhk:wq", long_options,
+                              &option_index)) != -1) {
+        switch (opt) {
+            case 'c':
+                certfile = optarg;
+                certfile_default = false;
+                break;
+            case 'f':
+                force_create = true;
+                break;
+            case 'h':
+                usage();
+                return (0);
+                break;
+            case 'k':
+                keyfile = optarg;
+                keyfile_default = false;
+                break;
+            case 'w':
+                create_cert = true;
+                break;
+            case 'q':
+                quiet = true;
+                break;
+            default:
+                // A message will have already been output about the error.
+                return (BAD_OPTIONS);
+        }
+    }
+
+    if (optind < argc) {
+        std::cout << "Error: extraneous arguments" << std::endl << std::endl;
+        usage();
+        return (BAD_OPTIONS);
+    }
+
+    // Some sanity checks on option combinations
+    if (create_cert && (certfile_default ^ keyfile_default)) {
+        std::cout << "Error: keyfile and certfile must both be specified "
+                     "if one of them is when calling b10-certgen in write "
+                     "mode." << std::endl;
+        return (BAD_OPTIONS);
+    }
+    if (!create_cert && !keyfile_default) {
+        std::cout << "Error: keyfile is not used when not in write mode"
+                  << std::endl;
+        return (BAD_OPTIONS);
+    }
+
+    // Initialize the tool and perform the appropriate action(s)
+    CertificateTool tool(quiet);
+    return (tool.run(create_cert, force_create, certfile, keyfile));
+}

+ 214 - 0
src/bin/cmdctl/b10-certgen.xml

@@ -0,0 +1,214 @@
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+               "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
+	       [<!ENTITY mdash "&#8212;">]>
+<!--
+ - Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+ -
+ - Permission to use, copy, modify, and/or distribute this software for any
+ - purpose with or without fee is hereby granted, provided that the above
+ - copyright notice and this permission notice appear in all copies.
+ -
+ - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+ - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ - AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+ - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+ - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ - PERFORMANCE OF THIS SOFTWARE.
+-->
+
+<refentry>
+
+  <refentryinfo>
+    <date>November 15, 2012</date>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>b10-certgen</refentrytitle>
+    <manvolnum>1</manvolnum>
+    <refmiscinfo>BIND10</refmiscinfo>
+  </refmeta>
+
+  <refnamediv>
+    <refname>b10-certgen</refname>
+    <refpurpose>X509 Certificate generation tool for use with b10-cmdctl</refpurpose>
+  </refnamediv>
+
+  <docinfo>
+    <copyright>
+      <year>2012</year>
+      <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
+    </copyright>
+  </docinfo>
+
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>b10-certgen</command>
+        <group choice="opt">
+          <arg choice="[OPTION]..."><option>-</option></arg>
+        </group>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>DESCRIPTION</title>
+    <para>The <command>b10-certgen</command> tool validates, creates, or
+      updates a self-signed X509 certificate for use in b10-cmdctl.
+    </para>
+
+    <para>
+      The connection between <command>bindctl</command> and
+      <command>b10-cmdctl</command> is done over HTTPS, and therefore
+      <command>b10-cmdctl</command> needs a certificate. Since these
+      certificates have expiry dates, they also need to be regenerated at
+      some point.
+
+      There are many tools to do so, but for ease of use, <command>
+      b10-certgen</command> can create a simple self-signed certificate.
+
+      By default, it will not create anything, but it will merely check an
+      existing certificate (if not specified, cmdctl-certfile.pem, in the
+      current working directory). And print whether it is valid, and
+      whether it would update if the option '-w' is given.
+
+      With that option, the certificate could then be replaced by a newly
+      created one. If the certificate is still valid, it would still not
+      be overwritten (however, if it is found to be invalid, for example
+      because it has expired, it would create a new one).
+
+      A new certificate is always created if the certificate file does
+      not exist, or if creation is forced (with the -f option).
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>ARGUMENTS</title>
+
+    <para>The arguments are as follows:</para>
+
+    <variablelist>
+
+      <varlistentry>
+        <term>
+          <option>-c <replaceable>file</replaceable></option>,
+          <option>--certfile=<replaceable>file</replaceable></option>
+        </term>
+        <listitem>
+          <para>
+            File to read the certificate from, or write the certificate to.
+            If <option>-w</option> and <option>-c</option> are used,
+            <option>-k</option> is mandatory as well.
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <option>-f</option>,
+          <option>--force</option>
+        </term>
+        <listitem>
+          <para>
+            Force updating of certificate when <option>-w</option> is used,
+            even if the existing certificate is still valid.
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <option>-h</option>,
+          <option>--help</option>
+        </term>
+        <listitem>
+          <para>
+            Print the command line arguments and exit.
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <option>-k <replaceable>file</replaceable></option>,
+          <option>--keyfile=<replaceable>file</replaceable></option>
+        </term>
+        <listitem>
+          <para>
+            File to write the private key to. This option is only valid when <option>-w</option> is used, and if this option is used, <option>-c</option> is mandatory as well.
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <option>-w</option>,
+          <option>--write</option>
+        </term>
+        <listitem>
+          <para>
+            Check the given certificate file. If it does not exist, a new
+            private key and certificate are created. If it does exist, the
+            certificate is validated. If it is not valid (for instance
+            because it has expired), it is overwritten with a newly created
+            certificate. If it is valid, nothing happens (use
+            <option>-f</option> to force an update in that case).
+          </para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <option>-q</option>,
+          <option>--quiet</option>
+        </term>
+        <listitem>
+          <para>
+            Don't print informational messages (only command-line errors are
+            printed). Useful in scripts when only the return code is needed.
+          </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>SEE ALSO</title>
+    <para>
+      <citerefentry>
+        <refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citetitle>BIND 10 Guide</citetitle>.
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>HISTORY</title>
+    <para>
+      The <command>b10-certgen</command> tool was first implemented
+      in November 2012 for the ISC BIND 10 project.
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>EXAMPLE</title>
+    <para>
+      To update an expired certificate in BIND 10 that has been installed to
+      /usr/local:
+      <screen>
+$> cd /usr/local/etc/bind10-devel/
+
+$> b10-certgen
+cmdctl-certfile.pem failed to verify: certificate has expired
+Running with -w would overwrite the certificate
+
+$> b10-certgen --write
+cmdctl-certfile.pem failed to verify: certificate has expired
+Creating key file cmdctl-keyfile.pem
+Creating certificate file cmdctl-certfile.pem
+
+$> b10-certgen --write
+cmdctl-certfile.pem is valid
+Not creating a new certificate (use -f to force)
+      </screen>
+    </para>
+  </refsect1>
+</refentry><!--
+ - Local variables:
+ - mode: sgml
+ - End:
+-->

+ 0 - 15
src/bin/cmdctl/cmdctl-keyfile.pem

@@ -1,15 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIICXAIBAAKBgQDpICWxJGKMvUhLFPbf5n8ZWogqjYcQqqoHqHVRHYjyiey6FZdt
-ZkY2s1gYh0G0NXtimlIgic+vEcFe7vdmyKntW7DYDaqAj0KrED7RKAj8324jNbSJ
-HtLP4evvJep3vsoNtTvNuceQJ46vukxyxgg3DuC9kVqPuD8CZ1Rq4ATyiwIDAQAB
-AoGBAOJlOtV+DUq6Y2Ou91VXRiU8GzKgAQP5iWgoe84Ljbxkn4XThBxVD2j94Fbp
-u7AjpDCMx6cbzpoo9w6XqaGizAmAehIfTE3eFYs74N/FM09Wg2OSDyxMY0jgyECU
-A4ukjlPwcGDbmgbmlY3i+FVHp+zCgtZEsMC1IAosMac1BoX5AkEA/lrXWaVtH8bo
-mut3GBaXvubZMdaUr0BUd5a9q+tt4dQcKG1kFqgCNKhNhBIcpiMVcz+jGmOuopNA
-8dnUGqv3FQJBAOqiJ54ZvOTWNDpJIe02wIXRxRmc1xhHFCqYP23KxBVrAcTYB19J
-lesov/hEbnGLCbKS/naZJ1zrTImUPNRLqx8CQCzDtA7U7GWhTiKluioFH+O7IRKC
-X1yQh80cPHlbT9VkzSfYSLssCmdWD35k6aHbntTPqFbmoD+AhveJjKi9BxkCQDwX
-1c+/RcrSNcQr0N2hZUOgyztZGRnlsnuKTMyA3yGhK23P6mt0PEpjQG+Ej0jTVGOB
-FF0pspQwy4R9C+tPif8CQH36NNlXBfVNmT7kDtyLmaE6pID0vY9duX56BJbU1R0x
-SQ8/LcfJagk6gvp08OyYCPA+WZ7u/bas9R/nMTCLivc=
------END RSA PRIVATE KEY-----

+ 7 - 2
src/bin/cmdctl/tests/Makefile.am

@@ -1,6 +1,9 @@
 PYCOVERAGE_RUN=@PYCOVERAGE_RUN@
-PYTESTS = cmdctl_test.py
+PYTESTS = cmdctl_test.py b10-certgen_test.py
 EXTRA_DIST = $(PYTESTS)
+EXTRA_DIST += testdata/expired-certfile.pem
+EXTRA_DIST += testdata/mangled-certfile.pem
+EXTRA_DIST += testdata/noca-certfile.pem
 
 # If necessary (rare cases), explicitly specify paths to dynamic libraries
 # required by loadable python modules.
@@ -9,10 +12,12 @@ if SET_ENV_LIBRARY_PATH
 LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
 endif
 
+CLEANFILES = test-keyfile.pem test-certfile.pem
+
 # test using command-line arguments, so use check-local target instead of TESTS
 check-local:
 if ENABLE_PYTHON_COVERAGE
-	touch $(abs_top_srcdir)/.coverage 
+	touch $(abs_top_srcdir)/.coverage
 	rm -f .coverage
 	${LN_S} $(abs_top_srcdir)/.coverage .coverage
 endif

+ 254 - 0
src/bin/cmdctl/tests/b10-certgen_test.py

@@ -0,0 +1,254 @@
+# Copyright (C) 2012  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# Note: the main code is in C++, but what we are mostly testing is
+# options and behaviour (output/file creation, etc), which is easier
+# to test in python.
+
+import unittest
+import os
+from subprocess import call
+import subprocess
+import ssl
+import stat
+
+def run(command):
+    """
+    Small helper function that returns a tuple of (rcode, stdout, stderr) after
+    running the given command (an array of command and arguments, as passed on
+    to subprocess).
+    """
+    subp = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    (stdout, stderr) = subp.communicate()
+    return (subp.returncode, stdout, stderr)
+
+class FileDeleterContext:
+    """
+    Simple Context Manager that deletes a given set of files when the context
+    is left.
+    """
+    def __init__(self, files):
+        self.files = files
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, type, value, traceback):
+        for f in self.files:
+            if os.path.exists(f):
+                os.unlink(f)
+
+class FilePermissionContext:
+    """
+    Simple Context Manager that temporarily modifies file permissions for
+    a given file
+    """
+    def __init__(self, f, unset_flags = [], set_flags = []):
+        """
+        Initialize file permission context.
+        See the stat module for possible flags to set or unset.
+        The flags are changed when the context is entered (i.e.
+        you can create the context first without any change)
+        The flags are changed back when the context is left.
+
+        Parameters:
+        f: string, file to change permissions for
+        unset_flags: list of flags to unset
+        set_flags: list of flags to set
+        """
+        self.file = f
+        self.orig_mode = os.stat(f).st_mode
+        new_mode = self.orig_mode
+        for flag in unset_flags:
+            new_mode = new_mode & ~flag
+        for flag in set_flags:
+            new_mode = new_mode | flag
+        self.new_mode = new_mode
+
+    def __enter__(self):
+        os.chmod(self.file, self.new_mode)
+
+    def __exit__(self, type, value, traceback):
+        os.chmod(self.file, self.orig_mode)
+
+def read_file_data(filename):
+    """
+    Simple text file reader that returns its contents as an array
+    """
+    with open(filename) as f:
+        return f.readlines()
+
+class TestCertGenTool(unittest.TestCase):
+    TOOL = '../b10-certgen'
+
+    def run_check(self, expected_returncode, expected_stdout, expected_stderr, command):
+        """
+        Runs the given command, and checks return code, and outputs (if provided).
+        Arguments:
+        expected_returncode, return code of the command
+        expected_stdout, (multiline) string that is checked agains stdout.
+                         May be None, in which case the check is skipped.
+        expected_stderr, (multiline) string that is checked agains stderr.
+                         May be None, in which case the check is skipped.
+        """
+        (returncode, stdout, stderr) = run(command)
+        self.assertEqual(expected_returncode, returncode, " ".join(command))
+        if expected_stdout is not None:
+            self.assertEqual(expected_stdout, stdout.decode())
+        if expected_stderr is not None:
+            self.assertEqual(expected_stderr, stderr.decode())
+
+    def validate_certificate(self, expected_result, certfile):
+        """
+        Validate a certificate, using the quiet option of the tool; it runs
+        the check option (-c) for the given base name of the certificate (-f
+        <certfile>), and compares the return code to the given
+        expected_result value
+        """
+        self.run_check(expected_result, '', '',
+                       [self.TOOL, '-q', '-c', certfile])
+        # Same with long options
+        self.run_check(expected_result, '', '',
+                       [self.TOOL, '--quiet', '--certfile', certfile])
+
+
+    def test_basic_creation(self):
+        """
+        Tests whether basic creation with no arguments (except output
+        file name) successfully creates a key and certificate
+        """
+        keyfile = 'test-keyfile.pem'
+        certfile = 'test-certfile.pem'
+        command = [ self.TOOL, '-q', '-w', '-c', certfile, '-k', keyfile ]
+        self.creation_helper(command, certfile, keyfile)
+        # Do same with long options
+        command = [ self.TOOL, '--quiet', '--write', '--certfile=' + certfile, '--keyfile=' + keyfile ]
+        self.creation_helper(command, certfile, keyfile)
+
+    def creation_helper(self, command, certfile, keyfile):
+        """
+        Helper method for test_basic_creation.
+        Performs the actual checks
+        """
+        with FileDeleterContext([keyfile, certfile]):
+            self.assertFalse(os.path.exists(keyfile))
+            self.assertFalse(os.path.exists(certfile))
+            self.run_check(0, '', '', command)
+            self.assertTrue(os.path.exists(keyfile))
+            self.assertTrue(os.path.exists(certfile))
+
+            # Validate the certificate that was just created
+            self.validate_certificate(0, certfile)
+
+            # When run with the same options, it should *not* create it again,
+            # as the current certificate should still be valid
+            certdata = read_file_data(certfile)
+            keydata = read_file_data(keyfile)
+
+            self.run_check(0, '', '', command)
+
+            self.assertEqual(certdata, read_file_data(certfile))
+            self.assertEqual(keydata, read_file_data(keyfile))
+
+            # but if we add -f, it should force a new creation
+            command.append('-f')
+            self.run_check(0, '', '', command)
+            self.assertNotEqual(certdata, read_file_data(certfile))
+            self.assertNotEqual(keydata, read_file_data(keyfile))
+
+    def test_check_bad_certificates(self):
+        """
+        Tests a few pre-created certificates with the -c option
+        """
+        if ('CMDCTL_SRC_PATH' in os.environ):
+            path = os.environ['CMDCTL_SRC_PATH'] + "/tests/testdata/"
+        else:
+            path = "testdata/"
+        self.validate_certificate(10, path + 'expired-certfile.pem')
+        self.validate_certificate(100, path + 'mangled-certfile.pem')
+        self.validate_certificate(17, path + 'noca-certfile.pem')
+
+    def test_bad_options(self):
+        """
+        Tests some combinations of commands that should fail.
+        """
+        # specify -c but not -k
+        self.run_check(101,
+                       'Error: keyfile and certfile must both be specified '
+                       'if one of them is when calling b10-certgen in write '
+                       'mode.\n',
+                       '', [self.TOOL, '-w', '-c', 'foo'])
+        self.run_check(101,
+                       'Error: keyfile and certfile must both be specified '
+                       'if one of them is when calling b10-certgen in write '
+                       'mode.\n',
+                       '', [self.TOOL, '-w', '-k', 'foo'])
+        self.run_check(101,
+                       'Error: keyfile is not used when not in write mode\n',
+                       '', [self.TOOL, '-k', 'foo'])
+        # Extraneous argument
+        self.run_check(101, None, None, [self.TOOL, 'foo'])
+        # No such file
+        self.run_check(105, None, None, [self.TOOL, '-c', 'foo'])
+
+    def test_permissions(self):
+        """
+        Test some combinations of correct and bad permissions.
+        """
+        keyfile = 'mod-keyfile.pem'
+        certfile = 'mod-certfile.pem'
+        command = [ self.TOOL, '-q', '-w', '-c', certfile, '-k', keyfile ]
+        # Delete them at the end
+        with FileDeleterContext([keyfile, certfile]):
+            # Create the two files first
+            self.run_check(0, '', '', command)
+            self.validate_certificate(0, certfile)
+
+            # Make the key file unwritable
+            with FilePermissionContext(keyfile, unset_flags = [stat.S_IWUSR]):
+                self.run_check(106, '', '', command)
+                # Should have no effect on validation
+                self.validate_certificate(0, certfile)
+
+            # Make the cert file unwritable
+            with FilePermissionContext(certfile, unset_flags = [stat.S_IWUSR]):
+                self.run_check(106, '', '', command)
+                # Should have no effect on validation
+                self.validate_certificate(0, certfile)
+
+            # Make the key file unreadable (this should not matter)
+            with FilePermissionContext(keyfile, unset_flags = [stat.S_IRUSR]):
+                self.run_check(0, '', '', command)
+
+                # unreadable key file should also not have any effect on
+                # validation
+                self.validate_certificate(0, certfile)
+
+            # Make the cert file unreadable (this should matter)
+            with FilePermissionContext(certfile, unset_flags = [stat.S_IRUSR]):
+                self.run_check(106, '', '', command)
+
+                # Unreadable cert file should also fail validation
+                self.validate_certificate(106, certfile)
+
+        # Not directly a permission problem, but trying to check or create
+        # in a nonexistent directory returns different error codes
+        self.validate_certificate(105, 'fakedir/cert')
+        self.run_check(103, '', '', [ self.TOOL, '-q', '-w', '-c',
+                                      'fakedir/cert', '-k', 'fakedir/key' ])
+
+if __name__== '__main__':
+    unittest.main()
+

src/bin/cmdctl/cmdctl-certfile.pem → src/bin/cmdctl/tests/testdata/expired-certfile.pem


+ 21 - 0
src/bin/cmdctl/tests/testdata/mangled-certfile.pem

@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDhzCCAvCgAwIBAgIJALwngNFik7ONMA0GCSqGSIb3DQEBBQUAMIGKMQswCQYD
+VQQGEwJjbjEQMA4GA1UECBMHYmVpamluZzEQMA4GA1UEBxMHYmVpamluZzEOMAwG
+A1UEChMFY25uaWMxDjAMBgNVBAsTBWNubmljMRMwEQYDVQQDEwp6aGFuZ2xpa3Vu
+MSIwIAYJKoZIhvcNAQkBFhN6aGFuZ2xpa3VuQGNubmljLmNuMB4XDTEwMDEwNzEy
+NDcxOFoXDTExMDEwNzEyNDcxOFowgYoxCzAJBgNVBAYTAmNuMRAwDgYDVQQIEwdi
+ZWlqaW5nMraWDgYDVQQHEwdiZWlqaW5nMQ4wDAYDVQQKEwVjbm5pYzEOMAwGA1UE
+CxMFY25uaWMxeZaRBgNVBAMTCnpoYW5nbGlrdW4xIjAgBgkqhkiG9w0BCQEWE3po
+YW5nbGlrdW5AY25UAwMuY24wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOkg
+JbEkYoy9SEsU9t/mfxLAICqNhxCqqgeodVEdiPKJ7LoVl21mRjazWBiHQbQ1e2Ka
+UiCJz68RwV7u92bIqe1bsNgNqoCPQqsQPtEoCPzfbiM1tIke0s/h6+8l6ne+yg21
+O825x5Anjq+6THLGCDcO4L2RWo+4PwJnVGrgBPKLAgMBAAGjgfIwge8wHQYDVR0O
+BBYEFJKM/O0ViGlwtb3JEci/DLTO/7DaMIG/BgNVHSMEgbcwgbSAFJKM/O0ViGlw
+tb3JEci/DLTO/7DaoYGQpIGNMIGKMQswCQYDVQQGEwJjbjEQMA4GA1UECBMHYmVp
+amluZzEQMA4GA1UEBxMHYmVpamluZzEOMAwGA1UEChMFY25uaWMxDjAMBgNVBAsT
+BWNubmljMRMwEQYDVQQDEwp6aGFuZ2xpa3VuMSIwIAYJKoZIhvcNAQkBFhN6aGFu
+Z2xpa3VuQGNubmljLmNuggkAvCeA0WKTs40wDAYDVR0TBAUwAwEB/zANBgkqhkiG
+9w0BAQUFAAOBgQBh5N6isMAQAFFD+pbfpppjQlO4vUNcEdzPdeuBFaf9CsX5ZdxV
+jmn1ZuGm6kRzqUPwPSxvCIAY0wuSu1g7YREPAZ3XBVwcg6262iGOA6n7E+nv5PLz
+EuZ1oUg+IfykUIoflKH6xZB4MyPL+EgkMT+i9BrngaXHXF8tEO30YppMiA==
+-----END CERTIFICATE-----

+ 19 - 0
src/bin/cmdctl/tests/testdata/noca-certfile.pem

@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDBjCCAe6gAwIBAgIRALUIj3nnW5uDE/+fglPvUDwwDQYJKoZIhvcNAQELBQAw
+HjELMAkGA1UEBhMCVVMxDzANBgNVBAMTBkJJTkQxMDAeFw0xMjExMTQxMjQ5MjVa
+Fw0xMzExMTQxMjQ5MjVaMB4xCzAJBgNVBAYTAlVTMQ8wDQYDVQQDEwZCSU5EMTAw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkIOPfs3Aw9kNDu1JqA2w3
+84/n9oUgAwAlHVmuJv7ZDw1MDaIKHjsh3DW09z+nv67GVksI7pFtAw5O4mnTDxpa
+JT0NKzhvYGfe8VdV/hWDogTIdk1QBJNZ2/id8z0h8z5001sARXPf+4mHBJslenH3
+YtZs22BG5RBLULtZ/2Nr7JkdfLlc6D5PCoDG22r1OiFkYVdCWfLDjisVIbSYPBtY
+BlKAIrvbmOtWcaGM+vQAhl0T5N8WRCKhaQH0DEmzQNckkYd7rSECo57KYiuvOdzp
+d+3bWTgGGy2ff0o3LZypv0O5s0TDC2H6hYtN4bUbcChUJbFu9b5sVZaOEVZtUsyD
+AgMBAAGjPzA9MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgTwMB0GA1UdDgQW
+BBSqGzsEDNs9E7gBL5pD6XVAwUo4DTANBgkqhkiG9w0BAQsFAAOCAQEAMTNB8NCU
+dnLFZ0jNpvecbECkX/OWGlBYU4/CsoNiibwp4CtUYS2A4NFVjWAyuzLSHhRQi0vJ
+CCWLpKL4VTkaDN5Oft42iUhvEXMnriJqpfXHnjCiBwFFSPl5WKfMIaRNK+tF4zbB
+F+FGNEEmYG3t/ni82orDLq4oy+7CoQwzZNzj5yoV6q7O9kLR9OMPNwJrc27A4erB
+7VMRZslSrNA4uA6YhMZl8iEvO1H801ct0zTxawrCihPOZOCSLew35xjztO7d3YH8
+YavOu5kzeu7AgZ2n75H/qU47ZgBjbonn9Osvrct+RIwZuWTB2bDML8JhNaZCq0aA
+TDBC0QWqIYypLg==
+-----END CERTIFICATE-----