PKI

One of the task I used to do while doing the job in telecommunication industry is developing application or script. Since the technology I handled is an IT based (UNIX, TCP/IP stack, etc), it is easy to integrate script for various automation purpose. Scripting is a must especially when doing operation task, you can’t imagine doing everything manually while the pressure and high load is usually the challenge we’re facing almost everyday.

One of fun challenge lately was developing a script for user management which use PKI (Public Key Infrastructure). There’re various application exist for this, one of them is EJBCA. Creating one-two user using application such as EJBCA is fine, there’re steps need to be done and involving open URL, copy-paste, download, openssl, etc. But if you’re requested to create more than 50 users it will be a hell. We’re human, and labour job should be done by computer, right?

Several options usually come when developing an apps or script, one of the first thing is which language should I use. The efficient way would be using bash script running on some server so everybody can access it immediately by logging into the server. But, this time I prefer ruby. There’s a reason for this, I’ll talk about this reason in another post later :).

So, why is it a fun challenge?because I never create something related to PKI before. I several times using openssl feature, but not really understand concept behind PKI. Learn how to automate the creation of user and its certificate force me to learn more about PKI and how it is work internally. That’s the fun part ;).

Here’s part of the script which relate to PKI stuff. Ruby is a HUMAN language, so even somebody never code in ruby, reading the source will give you the concept about PKI (hopefully heheh…).

#!/usr/bin/env ruby

# lib/qtel.rb
# modul for onends user-certs prov gateway management

require 'openssl'

module Qtel
  
  module Pgw
    
    # Generate CSR, sign with provisioning gateway CA
    def self.generateUserCert(username)
      
      resultdir = File.join(File.expand_path(File.dirname(__FILE__)), '../certs/')
      
      # Load the CA Cert
      ca_cert = OpenSSL::X509::Certificate.new File.read 'blah.pem'
      # Load the CA Key
      ca_key = OpenSSL::PKey::RSA.new File.read 'blah.key'
      
      # Creating user private/public key
      user_key = OpenSSL::PKey::RSA.new 1024
      # Write the user pkeys into a .pem file. Need to write into a file
      # in order to put into KeyStore later.
      open "#{resultdir}#{username}.pkey.pem", 'w' do |io|
        io.write user_key.to_pem
      end

      # Write user's privs keys into a file in .der format
      #open "#{username}.pkey.der", 'w' do |io|
       # io.write user_key.to_der
      #end

      # Creating user certificate signing request (csr)
      csr = OpenSSL::X509::Request.new
      csr.version = 0
      subject = OpenSSL::X509::Name.parse "CN=#{username}.ProvGW"
      csr.subject = subject
      csr.public_key = user_key.public_key  # Attach user's public key in CSR
      csr.sign user_key, OpenSSL::Digest::SHA1.new # Sign user's CSR using his pkeys

      # Creating signed certificate for user's csr
      csr_cert = OpenSSL::X509::Certificate.new
      csr_cert.serial = 0
      csr_cert.version = 2
      csr_cert.not_before = Time.now
      csr_cert.not_after = Time.local(2017) # All user's certs will be expire on Jan 01 2017
      csr_cert.subject = csr.subject
      csr_cert.public_key = csr.public_key
      csr_cert.issuer = ca_cert.subject
      ### Defining extension factory
      extension_factory = OpenSSL::X509::ExtensionFactory.new
      extension_factory.subject_certificate = csr_cert
      extension_factory.issuer_certificate = ca_cert
      extension_factory.create_extension 'basicConstraints', 'CA:FALSE'
      extension_factory.create_extension 'keyUsage', 'keyEncipherment, dataEncipherment, digitalSignature'
      extension_factory.create_extension 'subjectKeyIdentifier', 'hash'

      ###### EJBCA also add these: Subject Key Identifier, Authority Key Identifier, Enchanced Key Usage, Basic Constraints
      ###### but without adding those extension, created certs will still work.

      ### Signing the user's CSR using CA pkeys
      csr_cert.sign ca_key, OpenSSL::Digest::SHA1.new

      # Write the signed CSR into a file in .pem format
      open "#{resultdir}#{username}.cert.pem", 'w' do |io|
        io.write csr_cert.to_pem
      end

      # Write the signed CSR into a file in .der format
      open "#{resultdir}#{username}.cert.der", 'w' do |io|
        io.write csr_cert.to_der
      end
      
      ### Convert private key in .pem format into .der format using openssl, somehow convert directly using
      ### openssl implemented by ruby can't be used by ImportKey. Must be some bugs somewhere!!!
      system "openssl pkcs8 -topk8 -nocrypt -in #{resultdir}#{username}.pkey.pem -inform PEM -out #{resultdir}#{username}.pkey.der -outform DER"
      ### Run ImportKey using java interpreter
      system "java -Dkeystore=#{resultdir}#{username}.ImportKey ImportKey #{resultdir}#{username}.pkey.der #{resultdir}#{username}.cert.der #{username}"

    end

blah.pem and blah.key are CA certificate and CA priv keys created using EJBCA. The rest of the script is about creating a new user certificate and sign it using CA certificate created by EJBCA.

The last part of modul involving pkeys conversion .pem into .der directly using openssl, and using a java class to create a keystore. Actually this is the ‘bad part’ which I can’t get the reason until now. The system I am using here read certificate from a keystore, I can’t find sample of creating keystore using ruby. Most of the discussion always suggest to use openssl, that’s why for now I just use another apps for doing so. Another thing is, converting pkey into .der format somehow failed to be processed by ImportKey.class, I tried to compare the binary produce by openssl and produce by ruby script method to_der(), both have same output. It should be work fine but not in this case.

In above case, keystore creation using a java class from Joachim Karrer & Jens Carlberg. Here’s the source:



import java.security.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.DataInputStream;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.security.spec.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Collection;
import java.util.Iterator;

/**
 * ImportKey.java
 *
 * <p>This class imports a key and a certificate into a keystore
 * (<code>$home/keystore.ImportKey</code>). If the keystore is
 * already present, it is simply deleted. Both the key and the
 * certificate file must be in <code>DER</code>-format. The key must be
 * encoded with <code>PKCS#8</code>-format. The certificate must be
 * encoded in <code>X.509</code>-format.</p>
 *
 * <p>Key format:</p>
 * <p><code>openssl pkcs8 -topk8 -nocrypt -in YOUR.KEY -out YOUR.KEY.der
 * -outform der</code></p>
 * <p>Format of the certificate:</p>
 * <p><code>openssl x509 -in YOUR.CERT -out YOUR.CERT.der -outform
 * der</code></p>
 * <p>Import key and certificate:</p>
 * <p><code>java comu.ImportKey YOUR.KEY.der YOUR.CERT.der</code></p><br />
 *
 * <p><em>Caution:</em> the old <code>keystore.ImportKey</code>-file is
 * deleted and replaced with a keystore only containing <code>YOUR.KEY</code>
 * and <code>YOUR.CERT</code>. The keystore and the key has no password; 
 * they can be set by the <code>keytool -keypasswd</code>-command for setting
 * the key password, and the <code>keytool -storepasswd</code>-command to set
 * the keystore password.
 * <p>The key and the certificate is stored under the alias
 * <code>importkey</code>; to change this, use <code>keytool -keyclone</code>.
 *
 * Created: Fri Apr 13 18:15:07 2001
 * Updated: Fri Apr 19 11:03:00 2002
 *
 * @author Joachim Karrer, Jens Carlberg
 * @version 1.1
 **/
public class ImportKey  {
    
    /**
     * <p>Creates an InputStream from a file, and fills it with the complete
     * file. Thus, available() on the returned InputStream will return the
     * full number of bytes the file contains</p>
     * @param fname The filename
     * @return The filled InputStream
     * @exception IOException, if the Streams couldn't be created.
     **/
    private static InputStream fullStream ( String fname ) throws IOException {
        FileInputStream fis = new FileInputStream(fname);
        DataInputStream dis = new DataInputStream(fis);
        byte[] bytes = new byte[dis.available()];
        dis.readFully(bytes);
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        return bais;
    }
        
    /**
     * <p>Takes two file names for a key and the certificate for the key, 
     * and imports those into a keystore. Optionally it takes an alias
     * for the key.
     * <p>The first argument is the filename for the key. The key should be
     * in PKCS8-format.
     * <p>The second argument is the filename for the certificate for the key.
     * <p>If a third argument is given it is used as the alias. If missing,
     * the key is imported with the alias importkey
     * <p>The name of the keystore file can be controlled by setting
     * the keystore property (java -Dkeystore=mykeystore). If no name
     * is given, the file is named <code>keystore.ImportKey</code>
     * and placed in your home directory.
     * @param args [0] Name of the key file, [1] Name of the certificate file
     * [2] Alias for the key.
     **/
    public static void main ( String args[]) {
        
        // change this if you want another password by default
        String keypass = "importkey";
        
        // change this if you want another alias by default
        String defaultalias = "importkey";

        // change this if you want another keystorefile by default
        String keystorename = System.getProperty("keystore");

        if (keystorename == null)
            keystorename = System.getProperty("user.home")+
                System.getProperty("file.separator")+
                "keystore.ImportKey"; // especially this 😉


        // parsing command line input
        String keyfile = "";
        String certfile = "";
        if (args.length < 2 || args.length>3) {
            System.out.println("Usage: java comu.ImportKey keyfile certfile [alias]");
            System.exit(0);
        } else {
            keyfile = args[0];
            certfile = args[1];
            if (args.length>2)
                defaultalias = args[2];
        }

        try {
            // initializing and clearing keystore 
            KeyStore ks = KeyStore.getInstance("JKS", "SUN");
            ks.load( null , keypass.toCharArray());
            System.out.println("Using keystore-file : "+keystorename);
            ks.store(new FileOutputStream ( keystorename  ),
                    keypass.toCharArray());
            ks.load(new FileInputStream ( keystorename ),
                    keypass.toCharArray());

            // loading Key
            InputStream fl = fullStream (keyfile);
            byte[] key = new byte[fl.available()];
            KeyFactory kf = KeyFactory.getInstance("RSA");
            fl.read ( key, 0, fl.available() );
            fl.close();
            PKCS8EncodedKeySpec keysp = new PKCS8EncodedKeySpec ( key );
            PrivateKey ff = kf.generatePrivate (keysp);

            // loading CertificateChain
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            InputStream certstream = fullStream (certfile);

            Collection c = cf.generateCertificates(certstream) ;
            Certificate[] certs = new Certificate[c.toArray().length];

            if (c.size() == 1) {
                certstream = fullStream (certfile);
                System.out.println("One certificate, no chain.");
                Certificate cert = cf.generateCertificate(certstream) ;
                certs[0] = cert;
            } else {
                System.out.println("Certificate chain length: "+c.size());
                certs = (Certificate[])c.toArray();
            }

            // storing keystore
            ks.setKeyEntry(defaultalias, ff, 
                           keypass.toCharArray(),
                           certs );
            System.out.println ("Key and certificate stored.");
            System.out.println ("Alias:"+defaultalias+"  Password:"+keypass);
            ks.store(new FileOutputStream ( keystorename ),
                     keypass.toCharArray());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

}// KeyStore

Using above modul should be easy,

mainbase = __FILE__

$:.unshift(File.join(File.expand_path(File.dirname(mainbase)), 'lib'))

require 'qtel'

usersfile = "users.txt"

File.new(usersfile, 'r').each_line do |line|
  webGuiUser, soapUser = line.strip!.split(',')
  Qtel::Pgw::generateUserCert(webGuiUser)

Well, hopefully can give somebody an idea about how to use ruby for PKI stuff, or give details to anyone who want to know more about PKI by reading above source code :).

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s