Skip to content

Instantly share code, notes, and snippets.

@cbj4074
Last active December 20, 2023 15:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cbj4074/ab5826395e940ffc202dcb11004daa54 to your computer and use it in GitHub Desktop.
Save cbj4074/ab5826395e940ffc202dcb11004daa54 to your computer and use it in GitHub Desktop.
Creating a Certificate Authority for Self-Signing TLS Certificates with OpenSSL

Creating the CA (Certificate Authority)

There is quite a bit of preparation and configuration required to create a CA. Fortunately, once the configuration is correct, the remainder of the process is relatively simple.

Create necessary directories and files

Absent these directories, openssl will emit errors during certain steps throughout the overall process.

$ cd ~
$ mkdir --parents ./CAtest/demoCA
$ mkdir ./CAtest/demoCA/newcerts ./CAtest/demoCA/private
$ cd CAtest

It is necessary to choose a starting serial number for the CA. For our purposes, we can simply start with 01.

$ echo "01" > ./demoCA/serial

It should be noted, however, that this approach is discouraged in favor of generating a longer, more unique serial number:

Use the "-CAcreateserial -CAserial herong.seq" option to let "OpenSSL" to create and manage the serial number.

For a more thorough explanation of serial numbers, see (from the References section, below): http://www.herongyang.com/Cryptography/OpenSSL-as-CA-Manage-Serial-Number-when-Signing-CSR.html

We also need an empty index file, or OpenSSL will complain when signing certificates:

$ touch ./demoCA/index.txt

Enact minimum required OpenSSL configuration

Paste the following content into a new file named openssl.cnf (from within the CAtest directory that we created above), e.g.:

$ vim ./openssl.cnf

See below the snippet for a detailed explanation of these directives. In short, this is the minimum configuration required to generate certificates that include SANs (Subject Alternate Names).

[req]
prompt = no
distinguished_name  = req_dn
req_extensions      = v3_req

[req_dn]
commonName          = *.example.com
emailAddress       = webmaster@example.com
countryName            = US
organizationName       = Widgets Pty, LTD
organizationalUnitName     = Information Technology Division
localityName           = Portland
stateOrProvinceName        = Maine

[ca]
default_ca      = CA_default            # The default ca section

[CA_default]

dir             = ./demoCA              # Where everything is kept
certs           = $dir/certs            # Where the issued certs are kept
crl_dir         = $dir/crl              # Where the issued crl are kept
database        = $dir/index.txt        # database index file.
new_certs_dir   = $dir/newcerts         # default place for new certs.

certificate     = $dir/cacert.pem       # The CA certificate
serial          = $dir/serial           # The current serial number
crlnumber       = $dir/crlnumber        # the current crl number
crl             = $dir/crl.pem          # The current CRL
private_key     = $dir/private/cakey.pem# The private key
RANDFILE        = $dir/private/.rand    # private random number file

x509_extensions = usr_cert              # The extentions to add to the cert
name_opt        = ca_default            # Subject Name options
cert_opt        = ca_default            # Certificate field options
copy_extensions = copy

default_days    = 365                   # how long to certify for
default_crl_days= 30                    # how long before next CRL
default_md      = default               # use public key default MD
preserve        = no                    # keep passed DN ordering
policy          = policy_match
[ policy_match ]
countryName             = match
stateOrProvinceName     = match
organizationName        = match
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional
[ policy_anything ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ usr_cert ]

basicConstraints=CA:FALSE
nsComment                       = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1   = *.example.com

Crucial lines:

copy_extensions = copy

Unless this line is present (and un-commented), Subject Alternate Names will not be propagated from the [alt_names] section (or whatever it happens to be named -- the name is arbitrary, and must simply match the name given via, e.g., subjectAltName = @alt_names). This line is commented-out, by default, because it must be used with care (for reasons that, admittedly, the author does not fully understand).

#subjectAltName = @alt_names

When this line is commented-out, the [alt_names] section is ignored. This line should be un-commented when it is necessary to add one or more SANs to the certificate. SANs may consist of FQDNs and/or IP addresses.

policy          = policy_match

This is the default policy and it is fairly strict in that it requires much more rigorous matching than the other included-by-default policy, policy_anything. One "gotcha" with using the default policy, policy_match, is that it typically causes a character encoding mismatch error unless the default OpenSSL configuration is modified. This link explains the fundamental nature of the issue:

http://stackoverflow.com/a/8766819/1772379

I just ran into this problem. The root cause is a mismatch between the values of string_mask in the client's and the CA's openssl.cnf. The easy fix is to modify the client's value to match what the CA expects, then regenerate the CSR. The hard fix is to edit the CA's value and start a fresh CA.

Thus, if the error The stateOrProvinceName field needed to be the same in the CA certificate (Gloucestershire) and the request (Gloucestershire) arises, it is necessary either to a) implement the change to string_mask, as explained above, or b) use the policy_anything policy. Option b) can be employed in-line, using a command switch, which is demonstrated in the attendant examples, below.

Finally, it may be possible to slim-down this minimal configuration even further, but that would likely require combing through every single directive, eliminating it, and testing whether or not the entire process still works. It doesn't seem to be worth the effort as of this writing.

Move into CAtest directory

If the subsequent commands are entered and we're not in the correct location (as defined in the openssl.cnf file, nothing will function correctly.

So, let's get there:

$ cd ./demoCA

Create root CA private key

$ openssl genrsa -out ./private/cakey.pem 2048

Or the same thing with a password on the key:

$ openssl genrsa -des3 -out ./private/cakey.pem 2048

Create root CA certificate (and self-sign it)

$ openssl req -x509 -new -nodes -key ./private/cakey.pem -days 3650 -out cacert.pem

Signing Certificates

Moving up out of the demoCA directory

As with previous commands, being in the wrong directory causes failures.

$ cd ../

This should put us back in the CAtest directory.

Generate private key for end-user certificate

$ openssl genrsa -out ./private.key 2048

Generate CSR for end-user certificate

(This is commented-out because the -CAcreateserial switch syntax is obsoleted or invalid, but the example may have other value; it comes from the link in the References section, http://datacenteroverlords.com/2012/03/01/creating-your-own-ssl-certificate-authority/ .)

$ openssl x509 -req -in device.csr -CA root.pem -CAkey cakey.pem -CAcreateserial -out device.crt -days 1095

$ openssl req -new -out request.csr -key private.key -config openssl.cnf

Sign and issue certificate

$ openssl ca -config openssl.cnf -policy policy_anything -out newcert.pem -days 1024 -in request.csr

Misc. Issues

The index.txt file holds a history of all certificates that the CA has issued. Any attempt to generate a certificate whose inputs are identical to those of a certificate that the CA has already issued will result in a cryptic OpenSSL error.

If the need to expunge a previously-generated certificate expires while testing, simply delete the corresponding line from index.txt, save the file, and attempt certificate generation again.

Similarly, a copy of all certificates that the CA has issued are stored in the newcerts directory. If the need to "start over" with the CA should arise, the certificates in this directory can simply be deleted.

References

  1. http://www.xinotes.net/notes/note/1094/
  2. http://datacenteroverlords.com/2012/03/01/creating-your-own-ssl-certificate-authority/
  3. http://blog.simonandkate.net/20140411/self-signed-openssl-subjectaltname
  4. http://stackoverflow.com/questions/6976127/signing-a-certificate-with-my-ca
  5. http://www.herongyang.com/Cryptography/OpenSSL-as-CA-Manage-Serial-Number-when-Signing-CSR.html
  6. https://www.phildev.net/ssl/opensslconf.html
@MXMLN-sec
Copy link

Hi,
nice Tutorial :)

But why has your CA a lifetime of 1024 days:
$ openssl req -x509 -new -nodes -key cakey.pem -days 1024 -out cacert.pem

and your end-user certificate has a much longer lifetime of 3650 days?:
$ openssl ca -config openssl.cnf -policy policy_anything -out newcert.pem -days 3650 -in request.csr

Should be the other way around IMO.

@cbj4074
Copy link
Author

cbj4074 commented Dec 20, 2023

Hi, nice Tutorial :)

But why has your CA a lifetime of 1024 days: $ openssl req -x509 -new -nodes -key cakey.pem -days 1024 -out cacert.pem

and your end-user certificate has a much longer lifetime of 3650 days?: $ openssl ca -config openssl.cnf -policy policy_anything -out newcert.pem -days 3650 -in request.csr

Should be the other way around IMO.

Thank you!

And, yes, you're absolutory correct about swapping the validity period for the CA and leaf certificates. I probably copy/pasted those from somewhere and overlooked the discrepancy. I appreciate the feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment