Table of Contents

Introduction

After Heartbleed, I found myself in need of replacing a large number of SSL keypairs, most of which included SAN certificates. Of course, the first thing I did was try to script the process which resulted in some bashing of my head against my desk as I stumbled through the OpenSSL Ruby library.

But fret not, I’ll try to explain it as best I can and if you think I’ve made a mistake, I’m sure you will let me know in the comments below!

Assumptions and Prerequisites

I assume you are using a modern Ruby, version 2.1 or greater in this case. Though older versions may work, I have not tested any out. Let me know in the comments if you find another one works or doesn’t.

As for any gems we may need, the only one we pull in is the openssl-extensions gem.

Creating our Certificate Request

Including our Requirements

I may be in the minority, but I hate when I do not get the require statements I need as part of the post. Since this is my article I will do future me a favor and provide them here. You’re welcome future me.

require 'openssl'
require 'openssl-extensions/all'

Generating the Key Pair

Now we will generate our key pair. As you probably know, we need to provide the public key as part of our request then use the private key to sign the request.

key = OpenSSL::PKey::RSA.new 2048

keyfile = '/tmp/mycert.key'
file = File.new(keyfile,'w',0400)
file.write(key)
file.close

Generate the Request

Next up we will generate our request object. To do that, we first need to create our certificate subject as an OpenSSL::X509::Name object:

subj_arr = [ ['CN', 'myhost.example.com'], [ 'DC','example'], ['DC','com']]
subj      = OpenSSL::X509::Name.new(subj_arr)

Now, we create our request:

request = OpenSSL::X509::Request.new
request.version = 0
request.subject = subj
request.public_key = key.public_key

Now that we have our request, we need to setup our extensions and add them to it. This is the critical piece of this post since our SAN values are one of the extensions we need to add.

To begin, I found the following to be needed for basic SSL certificates. You may find different for your needs.

exts = [
  [ 'basicConstraints', 'CA:FALSE', false ],
  [ 'keyUsage', 'Digital Signature, Non Repudiation, Key Encipherment', false ],
]

Next we add our SAN extension to the request. First we need to format each SAN entry, then we’ll add them to our extension array:

sans = [ 'example.com', 'www.example.com' ]
sans.map! do |san|
  san = "DNS:#{san}"
end
exts << [ 'subjectAltName', sans.join(','), false ]

Now we need to convert our array into OpenSSL attributes, and add them to our request.

ef = OpenSSL::X509::ExtensionFactory
exts.map! do |ext|
  ef.create_extension(*ext)
end
attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(exts)])
attrs = [
  OpenSSL::X509::Attribute.new('extReq',attrval),
  OpenSSL::X509::Attribute.new('msExtReq',attrval),
]
attrs.each do |attr|
  request.add_attribute(attr)
end

Sign our Request

Finally, the very last thing we do is sign our request after we are done modifying it. If you do any other work on the request object in your own code, you need to make sure you do it before you get here.

request.sign(key, OpenSSL::Digest::SHA1.new)

# save our request to a file
csrfile = '/tmp/csrfile'
file = File.new(csrfile,'w',0400)
file.write(request)
file.close

# print out our request to screen for good measure
puts request.to_text

Code for this Example

Real World Example

Remember how I needed to write a tool in the face of the Heartbleed scramble? Well you can check out how I used the above code to write a tool that grabs an existing certificate and extract the information I need to generate a new key/certificate request based on it.

link: regenerate-cert

I was working on a server where the customer was using ClamAV and Stream Ports to check for viruses. They had a problem where the server would not accept their connection. The logfiles for the error showed up like:

Sun Jan  1 08:00:03 2012 -> ERROR: ScanStream 1088: accept() failed.

At first I thought it was a firewall rule, but after looking things over it looked ok. In my googling, I noticed a lot of people had problems with SELinux and since this system did in fact run SELinux, I started looking at those logfiles. I found the following errors (formatted for readability):

type=AVC msg=audit(1326835719.949:35310855): avc:  denied  { name_bind } for
    pid=4901 comm="clamd" src=1505 scontext=user_u:system_r:clamd_t:s0
    tcontext=system_u:object_r:port_t:s0 tclass=tcp_socket
type=SYSCALL msg=audit(1326835719.949:35310855): arch=c000003e syscall=49
    success=no exit=-13 a0=c a1=415d0e50 a2=10 a3=41a705af1fe3fb79 items=0
    ppid=1 pid=4901 auid=4294967295 uid=3218 gid=3218 euid=3218 suid=3218
    fsuid=3218 egid=3218 sgid=3218 fsgid=3218 tty=(none) ses=4294967295
    comm="clamd" exe="/usr/sbin/clamd" subj=user_u:system_r:clamd_t:s0
    key=(null)

This was one of my first rodeo’s with SELinux, so further research on the name_bind permission was necessary. I found that this occurs when an application tries to open a port they aren’t allowed to. I checked the SELinux configuration to see what ports it would allow ClamAV to open:

$ sudo semanage port -l | grep clamd
clamd_port_t tcp 3310

Bingo! The conf file for my client was defaulting to the streamports being open from 1024-2048, so I added that exception:

$ sudo semanage port -a -t clamd_port_t -p tcp 1024-2048 
$ sudo semanage port -l | grep 3310 
clamd_port_t tcp 1024-2048, 3310 

Once done, I tested…

$ telnet localhost 3310 Trying 127.0.0.1...
Connected to localhost.localdomain (127.0.0.1).
Escape character is '^]'.
STREAM
PORT 1246
^]

And confirmed it worked!