?

Log in

No account? Create an account
 
 
02 November 2008 @ 11:19 am
mini_ca: Create temporary/low security Certificate Authorities for SSL-based applications  
(Updated 2009-01-14)
Like most of my articles, this posting focuses on the script, and assumes you already know the basic technology behind it.  In this case, you need to understand how X.509 certificates work, as well as SSL, and what a Certificate Authority (CA) is (at least from a technology perspective, if not from the process and security side).  You also need to understand HERE documents in the shell, and how (and when) to escape variables and other special shell characters.

I actually run my own Certificate Authority using tinyCA, which I use to create and manage X.509 certificates and keys for my HTTPS, Secure SMTP, and Open VPN servers.  Although I don't lock the Root CA key in a safe, maintain a countersigned chain of custody log, nor did I videotape my key signing ceremony, I do treat it with same concern and circumspection that I apply to my ATM PIN, house keys, and bank account password.

Sometimes I need certificate & key material for purposes where I don't want to use my personal CA key, either because it's not that critical for it to remain secure, it's a one-time/temporary application, or I'm doing it for someone else (like a friend or employer).  Usually, this comes about when playing with stunnel or socat, and I need at least one certificate so I can use SSL/TLS for session encryption. So I finally automated the creation of a Certificate Authority and the request and signing of certificates by that CA.  I realize this has been done lots of times by lots of people, but I haven't seen one that creates a re-usable (but single-purpose) CA, nor one that is so convenient to use (again, for a single-purpose).

I call this tool mini_ca, and it works like this:

BEGIN USAGE

jdimpson@artoo:~$ mini_ca
usage: mini_ca <project_name>
END USAGE

There are two parts to the following example.  The first one creates the CA, called "tunnels_R_us". The second one uses it to create a single certificate for a user called "first_one".

BEGIN EXAMPLE1
jdimpson@artoo:~$ mini_ca tunnels_R_us
Created /home/jdimpson/tunnels_R_us-ca/tunnels_R_us-ca_openssl.cnf CA configuration file.
Creating tunnels_R_us CA certificate.
        Generating a 2048 bit RSA private key
        ..........++++++
        ........++++++
        writing new private key to '/home/jdimpson/tunnels_R_us-ca/private/cakey.pem'
        -----
Created tunnels_R_us-cacert.pem CA certificate
Created tunnels_R_us project certificate generator script "create_tunnels_R_us_user_cert.sh"
jdimpson@artoo:~$ mini_ca tunnels_R_us
Using existent CA configuration file /home/jdimpson/tunnels_R_us-ca/tunnels_R_us-ca_openssl.cnf.
Using existent CA certifcate tunnels_R_us-cacert.pem.
Created tunnels_R_us project certificate generator script "create_tunnels_R_us_user_cert.sh"
END EXAMPLE1


This example actually runs it twice, with the same argument each time.  The second time does NOT overwrite the CA certificate, key, or config file, but does re-write the user certificate script.  (Not sure if that's a bug or a feature.)

The major deliverables of this step are the CA's signing certificate, "tunnels_R_us-cacert.pem", and the "create_tunnels_R_us_ucer_cert.sh", which will be demo'd in Example2. All the data the new CA needs is in a directory called "tunnels_R_us-ca" in the current directory.  You can move this directory around--although you'll have to edit the appropriate path variable in create_tunnels_R_us_user_cert.sh file.   It doesn't depend on mini_ca, but it does still depend on openssl, though.

I tried to make the script proof against spaces in the project name, but I suspect it's not (because the OpenSSL config file that gets created probably won't like it), so don't use spaces.  Note that you have to set a password for the CA's private key.  You gotta remember this password.  I couldn't figure out how to override that behaviour  Fortunately, it's pretty easy to delete and re-create the CA if you forget it, although if you re-create the CA, all old certificates won't be valid in the new CA. 

On some systems I get an error message from openssl that says "unable to write 'random state'".  According to the OpenSSL FAQ,  "This message refers to the default seeding file (see previous answer). A possible reason is that no default filename is known because neither RANDFILE nor HOME is set. (Versions up to 0.9.6 used file ".rnd" in the current directory in this case, but this has changed with 0.9.6a.)"  I'm ignoring this, because I don't generally have strong security concerns when using this script  (really, because I'm lazy and ignorance is bliss). 


BEGIN EXAMPLE2
jdimpson@artoo:~$ ./tunnels_R_us-ca/create_tunnels_R_us_user_cert.sh
usage: create_tunnels_R_us_user_cert.sh <username>
jdimpson@artoo:~$ ./tunnels_R_us-ca/create_tunnels_R_us_user_cert.sh first_one
Creating key, req, & cert for first_one
        Generating a 2048 bit RSA private key
        .....++++++
        ................................++++++
        writing new private key to 'tunnels_R_us-first_one-key.pem'
        -----
        Using configuration from /home/jdimpson/tunnels_R_us-ca/tunnels_R_us-ca_openssl.cnf
        Check that the request matches the signature
        Signature ok
        The Subject's Distinguished Name is as follows
        domainComponent       :IA5STRING:'tunnels_R_us'
        stateOrProvinceName   :PRINTABLE:'New York'
        countryName           :PRINTABLE:'US'
        emailAddress          :IA5STRING:'first_one@localhost'
        organizationName      :T61STRING:'tunnels_R_us Users'
        commonName            :T61STRING:'user first_one'
        Certificate is to be certified until Dec 16 00:21:27 2010 GMT (700 days)
        Sign the certificate? [y/n]:
        1 out of 1 certificate requests certified, commit? [y/n]Write out database with 1 new entries
        Data Base Updated
        2+0 records in
        2+0 records out
        1024 bytes (1.0 kB) copied, 0.000486014 seconds, 2.1 MB/s
        0 semi-random bytes loaded
        Generating DH parameters, 512 bit long safe prime, generator 2
        This is going to take a long time
        ...+......................................................+.........+...........................+...+...........+.......................+................................................................+.+.+............+.........+........................+.....+.................++*++*++*++*++*++*
Use file /home/jdimpson/tunnels_R_us-ca/certs/tunnels_R_us-first_one.pem, it contains first_one's key, certicicate, and diffie hellman seed.

jdimpson@artoo:~$
END EXAMPLE2
 
Example2 runs the what I call the create user cert script.  The script was created in Example1, which put it in the CA directory.  In this case, the script name is "create_tunnels_R_us_user_cert.sh". To run it, just give it a unique name for the user for whom you want to create a certificate.  It delivers an all-in-one certificate file that contains the user's private key, certificate, and a Diffie Hellman parameters.  This example names it "tunnels_R_us-first_one.pem".  This file, along with the "tunnels_R_us-cacert.pem" from the first example, are the things most SSL tools (like socat and stunnel) want to have in order to do SSL.

By "user" I don't necessarily mean a person.  In fact, this tool wouldn't be a good choice for generated certificates for personal use (i.e. personal identification & authentication or for use in encrypted and signed email).  The user is just a way to uniquely ID each certificate generated.

Here's the code.  It makes use of the "openssl" utility, part of the OpenSSL library distribution.  This script is very ugly, because it auto-generates both an OpenSSL configuration file, and the create_user_cert.sh file.  It uses HERE documents to do that, and requires some awkward & confusing escaping in both in order to get the desired results.  So it's ugly and ungainly, but otherwise it's pretty simple, because it isn't much more than a wrapper around the previously mentioned openssl command.

BEGIN CODE
#!/bin/sh

PREF=$1;
if test -z "$PREF"; then
        #PREF="my";
        ME=`basename $0`;
        echo "usage: $ME <project_name>";
        exit 1;
fi;

CADIR=`pwd`/$PREF-ca
CA_CONF="$CADIR/$PREF-ca_openssl.cnf"
PASS_FILE="$CADIR/$PREF-ca_password";
CADAYS=100000
CERTBITS=2048
DHBITS=512
CACERT="$PREF-cacert.pem"
CERT_USER_SCRIPT="create_${PREF}_user_cert.sh";

indent () {
        local N=$1;
        local C=$2;
        [ -z "$N" ] && N=1;
        [ -z "$C" ] && C="      ";
        local ARGS;
        for i in `seq $N`; do
                ARGS="$ARGS s/^/$C/;";
        done
        sed -e "$ARGS";
}

###
# ca dir setup
###
mkdir -p "$CADIR" 2> /dev/null
cd "$CADIR" 2> /dev/null || exit 1;
mkdir -p "$CADIR/certs/" 2> /dev/null
mkdir -p "$CADIR/private/" 2> /dev/null
chmod go-rwx "$CADIR/private/";
touch "$CADIR/private/index.txt";
echo "1234" > "$PASS_FILE";

###
# create config file
###
if [ ! -e "$CA_CONF" ]; then
        cat > "$CA_CONF" << HERE
# CA configuration for $PREF project
#
####################################################################
[ ca ]
default_ca      = CA_default            # The default ca section
####################################################################
[ CA_default ]
dir             = $CADIR                        # Where everything is kept
certificate     = \$dir/$CACERT         # The CA certificate
database        = \$dir/private/index.txt       # database index file.
new_certs_dir   = \$dir/certs                   # default place for new certs.
private_key     = \$dir/private/cakey.pem       # The private key
certs           = \$dir/certs                   # Where the issued certs are kept
serial          = \$dir/private/serial          # The current serial number
crl_dir         = \$dir/crl                     # Where the issued crl are kept
default_days    = 700                           # how long to certify for
default_crl_days= 300                           # how long before next CRL
default_md      = md5                           # which md to use.
policy          = CertAuthCA_policy
x509_extensions = certificate_extensions        # The extentions to add to the cert

# For the CA policy
[ CertAuthCA_policy ]
commonName              = supplied
stateOrProvinceName     = supplied
countryName             = supplied
emailAddress            = supplied
organizationName        = supplied
organizationalUnitName  = optional

[ certificate_extensions ]
basicConstraints=CA:FALSE
####################################################################
[ req ]
default_bits            = $CERTBITS
default_keyfile         = $CADIR/private/cakey.pem
defualt_md              = md5
prompt                  = no
distinguished_name      = root_ca_distinguished_name
x509_extensions         = root_ca_extensions

[ root_ca_distinguished_name ]
commonName              = CA for a $PREF project
stateOrProvinceName     = New York
countryName             = US
emailAddress            = $PREF@localhost
organizationName        = $PREF Users

[ root_ca_extensions ]
basicConstraints        = CA:true
HERE
        echo "Created $CA_CONF CA configuration file.";
else
        echo Using existent CA configuration file $CA_CONF.;
fi

###
# create the CA cert
###
if [ ! -e "$CACERT" ]; then
        echo Creating $PREF CA certificate.
        openssl req -config $CA_CONF -x509 -newkey rsa:$CERTBITS -out $CACERT -outform PEM -days $CADAYS -passout "file:$PASS_FILE" 2>&1 | indent
        # openssl x509 -in $CACERT -text -noout
        echo 00 > "$CADIR/private/serial";
        echo "Created $CACERT CA certificate";
else
        echo Using existent CA certifcate $CACERT.;
fi

###
# create user certifcate req/gen script
###
cat > "$CERT_USER_SCRIPT" <<HERE
#!/bin/sh

CADIR="$CADIR"
CA_CONF="$CA_CONF"
CACERT="$CACERT"
UDIR="$CADIR/users"

indent () {
        local N=\$1;
        local C=\$2;
        [ -z "\$N" ] && N=1;
        [ -z "\$C" ] && C="     ";
        local ARGS;
        for i in \`seq \$N\`; do
                ARGS="\$ARGS s/^/\$C/;";
       done
        sed -e "\$ARGS";
}

mkdir -p "\$UDIR" 2>/dev/null
cd "\$UDIR" || exit 1;

umask 0066;
user=\$1;

if [ -z "\$user" ]; then
        ME=\`basename \$0\`;
        echo "usage: \$ME <username>";
        exit 1;
fi
shift;

subj="/DC=${PREF}/stateOrProvinceName=New York/countryName=US/emailAddress=\$user@localhost/organizationName=$PREF Users/CN=user \$user"

echo "Creating key, req, & cert for \$user";

KEY="$PREF-\$user-key.pem";
REQ="$PREF-\$user-req.pem";
CERT="$PREF-\$user-cert.cert";
DH="$PREF-\$user-dh.pem";
OUT="$PREF-\$user.pem";

if [ -e "\$KEY" -a -e "\$REQ" ]; then
        echo "A key and request for this user already exists. Will reuse.";
else
        openssl req -subj "\$subj" -newkey rsa:$CERTBITS -keyout "\$KEY" -keyform PEM -out "\$REQ" -outform PEM -nodes 2>&1 | indent 1
        #openssl req -in "\$REQ" -text
fi

yes | openssl ca -config "\$CA_CONF" -in "\$REQ" -passin "file:$PASS_FILE" -notext -out "\$CERT" 2>&1 | indent 1
#openssl x509 -config "\$CA_CONF" -in "\$CERT" -text

( dd if=/dev/urandom count=2 | openssl dhparam -rand - $DHBITS > "\$DH" ) 2>&1 | indent 1;

cat "\$KEY" "\$CERT" "\$DH" > "\$CADIR/certs/\$OUT";
chmod 600 "\$CADIR/certs/\$OUT";
echo "Use file \$CADIR/certs/\$OUT, it contains \${user}'s key, certicicate, and diffie hellman seed."
HERE

chmod a+x "$CERT_USER_SCRIPT";
echo "Created $PREF project certificate generator script \"$CERT_USER_SCRIPT\"";
END CODE

As I said, it's very simple:  first, it sets up a directory to put the CA files.  These include a private directory where the CA's private key will live; a certs directory where (one copy of) the generated certificates will live; the CA cert password (hardcoded to "1234"!!) and does a bit of houskeeping by creating files and setting permissions to make the openssl tool happy.  Second, it generates the OpenSSL configuration file.  I haven't stopped to fully understand all the settings in the OpenSSL config file or in the creation of the user certificate request, but I know they work for creating SSL certs for Apache web server, stunnel, socat, and OpenVPN.  Third, it creates the CA's key and certificate.  It creates 2048 bit certificates, which you can change my modifying $CERTBITS, and it makes the valid for 100000 days, which you can change by modifying $CADAYS.  Finally, it generates another script that knows how to use the newly created CA to create user certificates.

This autogenerated script is relatively simple as well.  It generates a private key and  a certificate request for the user, and immediately has the CA key sign the request.  It uses the "yes" command to automatically answer "y" when prompted for permission to sign the certificate.  Then it generates the the Diffie Helman parameters.  Finally, it concatenates the private key, the certificate, and the DH parameters into a single file.  It also saves every individual file in the users directory, but you don't really need them anymore once you have the concatenated file.

Both scripts have a shell function called indent defined.  Indent inserts one or more tabs at the beginning of each line of standard input it receives, then passes the input along to standard output.  I use it to offset the output and error from  calls to the openssl tool, so that I can visually distinguish between what openssl says and what my script says.

Now you have a user's concatenated key-cert-dh file that can be authenticated with the CA's certificate.  I'm planning future postings that will require this ability.

You can dowload mini_ca here: http://impson.tzo.com/~jdimpson/bin/mini_ca.

Edit 2009-01-14: Some updates, including typo fix, adding link to mini_ca, creation of the "indent" function, parameterization of the certificate key bit length, and removal of prompting from openssl for CA password and permission to sign certificates.