In this article, I wanted to go through a Python script I created recently, that generates keys and self-signed certificates using OpenSSL. The reason why I wrote the script was to make the generation of multiple keys, certificate signing requests and certificates for test environments easier. I discussed how to make this process a little faster using OpenSSL and a configuration file, however I still felt like it still was a little cumbersome.
If you want to skip the walk-through, you can find the script and a brief outline on GitHub.
Script outline
The script has three main parts, the argument parser, in certautomator.py, execution of the OpenSSL commands in cryptocmds.py and finally the configuration file. If you want, you can have a further look at the two aforementioned Python files on GitHub, but since most of the work will be done in the configuration file, I’m going to cover that below.
Configuration file overview
I decided to use JSON as the configuration file layout. JSON offers good readability, is easy to write and lightweight.
To increase flexibility, the configuration file supports multiple groups containing different parameters and values. Using groups makes it easier to generate different test environments or experimenting with different OpenSSL properties.
Each group contains a set of default values, divided into two objects, ssl_defaults and name_defaults.
- ssl_defaults contains values specific to generating keys, signing requests and certificates.
- name_defaults contains certificate specific information, such as country, state etc.
Lastly, in each group contains one or more CAs and one or more users, which are further separated in sub-groups.
For each of the CAs and users, the information in the ssl_defaults and name_defaults can be overwritten. So, its possible to choose which values to override and which to inherit. However, unless otherwise specified, each user will default to what is specified in ssl_defaults and name_defaults.
One thing to note is the need for mandatory filenames for keys and certificates for each user. They must be specified to prevent accidental overwriting of existing files.
Configuration outline
Here, we’ll walk through the structure of the configuration file. Later on, we’ll have a look at a couple of examples.
We start by adding a group name object that will hold all the values for the CAs and users in that group.
{ <group_name>:{ ... } }
Within each group there is an ssl_defaults object with the necessary elements as shown below.
<ssl_defaults> : { "bits" : <positive number optional default 2048>, "days" : <positive number optional default 365>, "protected" : <true/false optional, default false>, "password" : <string, required if protected>, "password_file" : <file location, required if protected>, "message_digest" : <optional default sha256>, "user_dir" : <directory mandatory>, "ca_dir": <directory mandatory> }
Additionally, each group must have a name_defaults object, with the necessary elements.
<name_defaults> : { "country": <string mandatory>, "state": <string mandatory>, "locality": <string mandatory>, "organization_name": <string mandatory>, "organizational_unit_name": <string mandatory>, "email": <string mandatory> },
Then, we specify the CA and the users within their respective groups.
<ca> : { <ca_name> : { "common_name": <string mandatory>, "key_name": <filename string mandatory>, "cert_name":<filename string mandatory>, "cert_request_name": <filename string mandatory> "country": <string optional overrides name_defaults>, "state": <string optional overrides name_defaults>, "locality": <string optional overrides name_defaults>, "organization_name": <string optional overrides name_defaults>, "organizational_unit_name": <string optional overrides name_defaults>, "email": <string optional overrides name_defaults>, "ca_conf":<location with filename, optional>, "ca_dir": <directory optional overrides ssl_defaults> "protected": <true|false optional, default false>, "password":<password string, required if protected is true>, "password_file":<location of password file, required if protected is true>, } }, <users> : { <user_name> : { "common_name": <string mandatory>, "key_name": <filename string mandatory>, "cert_name":<filename string mandatory>, "cert_request_name": <filename string mandatory> "country": <string optional overrides name_defaults>, "state": <string optional overrides name_defaults>, "locality": <string optional overrides name_defaults>, "organization_name": <string optional overrides name_defaults>, "organizational_unit_name": <string optional overrides name_defaults>, "email":<string optional overrides name_defaults> "user_dir":<string optional overrides ssl_defaults user_dir> "protected":<true|false optional, default false>, "password":<password string, required if protected is true>, "password_file":<location of password file, required if protected is true> } }
Finally, when we combine it all together, our configuration file should look like this.
{ <group_name> : { <ssl_defaults> : { "bits" : <positive number optional default 2048>, "days" : <positive number optional default 365>, "protected" : <true/false optional, default false>, "password" : <string, required if protected>, "password_file" : <file location, required if protected>, "message_digest" : <optional default sha256>, "user_dir" : <directory mandatory>, "ca_dir": <directory mandatory> }, <name_defaults> : { "country": <string mandatory>, "state": <string mandatory>, "locality": <string mandatory>, "organization_name": <string mandatory>, "organizational_unit_name": <string mandatory>, "email": <string mandatory> }, <ca> : { <ca_name> : { "common_name": <string mandatory>, "key_name": <filename string mandatory>, "cert_name":<filename string mandatory>, "cert_request_name": <filename string mandatory> "country": <string optional overrides name_defaults>, "state": <string optional overrides name_defaults>, "locality": <string optional overrides name_defaults>, "organization_name": <string optional overrides name_defaults>, "organizational_unit_name": <string optional overrides name_defaults>, "email": <string optional overrides name_defaults>, "ca_conf":<location with filename, optional>, "ca_dir": <directory optional overrides ssl_defaults> "protected": <true|false optional, default false>, "password":<password string, required if protected is true>, "password_file":<location of password file, required if protected is true>, } }, <users> : { <user_name> : { "common_name": <string mandatory>, "key_name": <filename string mandatory>, "cert_name":<filename string mandatory>, "cert_request_name": <filename string mandatory> "country": <string optional overrides name_defaults>, "state": <string optional overrides name_defaults>, "locality": <string optional overrides name_defaults>, "organization_name": <string optional overrides name_defaults>, "organizational_unit_name": <string optional overrides name_defaults>, "email": <string optional overrides name_defaults> "user_dir":<string optional overrides ssl_defaults user_dir> "protected": <true|false optional, default false>, "password":<password string, required if protected is true>, "password_file":<location of password file, required if protected is true>, }..., <user_name n> : { ... } } }, <group name n> : { ... } }
The Python script
Now with the configuration file structure described, we’ll look at the script parameters and how they work.
Required parameters
- –all / -a : performs all of the actions below.
- –key / -k : generates a keypair.
- –req / -r : generates a certificate signing request, if a key is present.
- –sign / -s : signs a certificate signing request.
One or more of these parameters must be specified when running the script. Additionally, -r will fail if a key cannot be found, and -s will fail if there is no signing request and/or no Certificate Authority defined and/or valid.
Optional parameters
There is also a couple of optional parameters, which can be useful.
- —verbose / –v : Logs debug messages.
- —quiet / –q : Logs warning messages only.
- —config : Location of the JSON configuration file, defaults to ./config.json.
- —log : Location of the log file, defaults to ./certautomator.log.
- —openssl : Location of the openssl binaries, defaults to /usr/bin/openssl.
- —overwrite : Will overwrite keys, requests or certificates files if they exists. (WARNING: This will overwrite all existing files without prompting. Use with caution)
- —group : Specify, using ‘,’ separator, select specific groups.
- —users : Specify, using ‘,’ separator, select specific users (across different groups).
Configuration file examples
As mentioned earlier, I wanted to outline a couple of different configuration file examples to get you started. Though fairly basic, both explore a couple of different ways to run the Python script.
{ "A": { "ssl_defaults": { "bits": 2048, "days": 90, "protected": false, "message_digest": "sha512", "user_dir": "./test_dir", "ca_dir": "./test_dir/ca" }, "name_defaults": { "country": "US", "state": "State", "locality": "City", "organization_name": "Company", "organizational_unit_name": "IT Dept", "email": "test@test.com" }, "ca": { "ca": { "common_name": "ca", "key_name": "ca.key", "cert_name": "ca.crt", "cert_request_name": "ca.csr" } }, "users": { "server1": { "common_name": "server1", "key_name": "server1.key", "cert_name": "server1.crt", "cert_request_name": "server1.csr" } } },
This configuration file will generate a self-signed Certificate Authority (CA) and one user. For the CA named ca, a key pair and a self-signed certificate will be generated. Then, when the keypair and signing request for server1 has been generated, ca will sign the certificate request and generate the certificate. The certificates will be valid for 90 days. Each key pair will be generated with a 2048 bit size, and the message digest is sha512.
Directory structure
A thing to note is the directory structure. When the script runs, it will create additional directories to hold the keys, signing requests and certificates. The example above will generate a new directory in the current directory called test_dir. Then, inside test_dir a directory called ca is created. Within test_dir and test_dir/ca, the directories keys, csrs and crts will be created. If these directories already exists, the script will continue as normal. Also, if required, you can specific a canonical path.
Finally, the last example shows multiple groups with multiple users.
Example 2
{ "A": { "ssl_defaults": { "bits": 2048, "days": 365, "protected": false, "message_digest": "sha512", "user_dir": "./A", "ca_dir": "./A/ca" }, "name_defaults": { "country": "US", "state": "State", "locality": "City", "organization_name": "Company A", "organizational_unit_name": "IT Dept", "email": "test@companyA.com" }, "ca": { "A_ca": { "common_name": "A_ca", "key_name": "A_ca.key", "cert_name": "A_ca.crt", "cert_request_name": "A_ca.csr" } }, "users": { "client1": { "common_name": "client1", "key_name": "client1.key", "cert_name": "client1.crt", "cert_request_name": "client1.csr" }, "server": { "common_name": "server", "key_name": "server.key", "cert_name": "server.crt", "cert_request_name": "server.csr" }, "client_2": { "common_name": "client_2", "key_name": "client_2.key", "cert_name": "client_2.crt", "cert_request_name": "client_2.csr" } } }, "B": { "ssl_defaults": { "bits": 2048, "days": 365, "protected": false, "message_digest": "sha256", "user_dir": "./B", "ca_dir": "./B/ca2" }, "name_defaults": { "country": "US", "state": "State", "locality": "City", "organization_name": "Company B", "organizational_unit_name": "Sales", "email": "admin@companyB.com" }, "ca": { "B_CA": { "common_name": "B_CA", "key_name": "B_CA.key", "cert_name": "B_CA.crt", "cert_request_name": "B_CA.csr", "protected": true, "password": "1234" } }, "users": { "B_Server": { "common_name": "B_Server", "protected":true, "password_file":"./password.txt", "key_name": "B_Server.key", "cert_name": "B_Server.crt", "cert_request_name": "B_Server.csr" } } } }
In this example, we’ll add in some password protection on the key pair for group B. Two examples here, one using password directly in the configuration file for B_CA and another where we use a password in a file called password.txt. Inside password.txt you would specify your password, and that is the password you would use when accessing the key pair.
Creating keys and certificates examples
Using the first configuration file, we’ll run through the process of generating the keys and the certificates, with the following command:
python certautomator -a --config example1.json
(The example assumes that the first example is saved using the filename example1.json)
Now you should have a directory called test_dir containing the keys and certificates for both the CA and Server1. This process can also be broken down into three individual steps;
python certautomator -k --config example1.json python certautomator -r --config example1.json python certautomator -s --config example1.json
First step generates the key pair for both users, second step generates the certificate signing requests and finally, we create and sign the certificates.
Keys and certificates for specific users
If we wanted to run the script for a set of users or just one group, we can supply the users or group flag. So, assuming you added a new ca entry and a new user entry called ca2 and server2. Now, you can select to generate the files for just those two users.
python certautomator -a --config example1.json --users "ca2,server2"
Now, the script will ignore the other users specified in the script and generate the keys, requests and certificate for ca and server2.
Summary
Hopefully this post wasn’t too long, though I felt that it was important to add a couple of examples on how to run the script. If you have any questions, or if there is additional examples you would like me to add, let me know in the comments section or send me an email. You can also contact me via the GitHub page.
Leave a Reply