Hello Guys,
In this tutorial we are going to implement 2 way or mutual ssl authentication. I would love to keep this post short and straight to the point so it assumes that you understand what SSL is for and what we are aiming for. But in every day english it means 2 parties authenticating to each other using an SSL certificate so both parties are assured of each other’s identities.
How does that work ?
Most of the time a web application has an SSL on its url and our browsers know how to verify that authenticity and let us through. Our browser which is a client to that web application has been able to verify the web application authenticity, that’s one way. The other way of the mutual ssl authentication is to make the web application able to authenticate its clients. Clients could be anything from a curl command, a python, java, ruby etc application as well as a simple browser. When that’s done we have a mutual ssl authentication.
There are several ways of doing this but we will use a simple but yet solid approach to implement this. A web server running in front of our actual application be it java or python ruby etc. We will use a spring boot application in this tutorial.
Requirements
- A Simple Spring boot application : A simple application that queries Internet Chuck Norris jokes database and runs on port 8080
- A nginx web server : Our web application which runs on port 80 and passes requests to the spring boot app. nginx is our reverse proxy.
- CompanyACA.crt :Company A Certficate Authority used to sign companyA clients’ certificates
- CompanyBorSomeCA.crt : Company B or Some Certificate Authority like verisign, digicert, thawte, geotrust used to sign jokes.mycodingpains.com server certificate
- jokes.mycodingpains.com.crt : SSL certificate for our application running at jokes.mycodingpains.com
- nejifromcompanyA.crt: client SSL certificate from companyA and signed by companyA to make call to our jokes application.
Our Scenario
We have a small java based app that makes requests to the Internet Chuck Norris Database to retrieve chuck norris famous jokes. It accepts an integer number on the application/x-www-form-urlencoded param “count” and retrieves the corresponding number of jokes. Nothing fancy ,with limited features and error handling. We want to make that application accessible from the subdomain jokes.mycodingpains.com. Because I didn’t want to handle adding certificate stuff to java nor dreaming about adding client’s certificate to java truststore, I would rather let nginx worry about that headache for me.
We want to only allow trusted client to be able to access those jokes so we will implement a mutual ssl authentication between the jokes app and any client that wants/needs access. Here comes companyA which we want to give access to. We will ask company A to sign its clients and send us an authenticating certificate we can use on the jokes app side to validate the client’s authenticity. Let’s see step by step how to make that happen
Generating everything client side: CompanyA
CompanyA Root Certificate
certificate key
Using openssl we are generating the key needed for the Root Certificate. This root certificate act as Certificate Authority for companyA.
1 2 3 4 5 6 7 |
openssl genrsa -des3 -out CompanyACA.key 4096 Generating RSA private key, 4096 bit long modulus ......................++ ....................................++ e is 65537 (0x10001) Enter pass phrase for CompanyACA.key: Verifying - Enter pass phrase for CompanyACA.key: |
I used a pass phase on the key which is 1234567890
Creating CompanyA Certificate
Let’s create the actual Root certificate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
openssl req -new -x509 -days 365 -key CompanyACA.key -out CompanyACA.crt Enter pass phrase for CompanyACA.key: You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:GH State or Province Name (full name) [Some-State]:Greater Accra Locality Name (eg, city) []:Accra Organization Name (eg, company) [Internet Widgits Pty Ltd]:CompanyA Organizational Unit Name (eg, section) []:CompanyA DevOps Common Name (e.g. server FQDN or YOUR name) []:root.companya.com Email Address []:security@companya.com |
Client Certificate
certificate key
Let’s create the key for the client certificate
1 2 3 4 5 6 7 8 9 10 11 |
openssl genrsa -out nejifromclientA.key 2048 Generating RSA private key, 2048 bit long modulus .......+++ ............................................................+++ e is 65537 (0x10001) rm nejifromclientA.key openssl genrsa -out nejifromcompanyA.key 2048 Generating RSA private key, 2048 bit long modulus ...................................................+++ .....................................................................+++ e is 65537 (0x10001) |
No pass phrase is used here notice -des3 is taken out from the genrsa command.
Creating nejifromcompanyA client Certificate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
openssl req -new -key nejifromcompanyA.key -out nejifromcompanyA.csr You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:GH State or Province Name (full name) [Some-State]:Greater Accra Locality Name (eg, city) []:Accra Organization Name (eg, company) [Internet Widgits Pty Ltd]:CompanyA Organizational Unit Name (eg, section) []:Hyuga Clan Common Name (e.g. server FQDN or YOUR name) []:neji.companya.com Email Address []:neji@companya.com Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: |
Signing of Neji client’s certificate by companyA using CompanyACA.crt
We will now use the Root Certificate from company A to sign the client generated by neji
1 2 3 4 5 |
openssl x509 -req -days 365 -in nejifromcompanyA.csr -CA CompanyACA.crt -CAkey CompanyACA.key -set_serial 01 -out nejifromcompanyA.crt Signature ok subject=/C=GH/ST=Greater Accra/L=Accra/O=CompanyA/OU=Hyuga Clan/CN=neji.companya.com/emailAddress=neji@companya.com Getting CA Private Key Enter pass phrase for CompanyACA.key: |
Now that we are done with the client’s certificate. Let’s see how server side is going to be like
Generating everything server side: CompanyBorSomeCA
Root Certificate
This step is not needed if you already have a signed certificate by any big and recognized Certificate Authority.
Creating the CompanyBorsomeCA key
1 2 3 4 5 6 7 |
openssl genrsa -des3 -out CompanyBorSomeCA.key 4096 Generating RSA private key, 4096 bit long modulus ............................................................................................................................++ ..................++ e is 65537 (0x10001) Enter pass phrase for CompanyBorSomeCA.key: Verifying - Enter pass phrase for CompanyBorSomeCA.key: |
For the key pass phrase I used 0987654321
Creating the Root Certificate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
openssl req -new -x509 -days 365 -key CompanyBorSomeCA.key -out CompanyBorSomeCA.crt Enter pass phrase for CompanyBorSomeCA.key: You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:GH State or Province Name (full name) [Some-State]:Greater Accra Locality Name (eg, city) []:Accra Organization Name (eg, company) [Internet Widgits Pty Ltd]:CompanyBorSomeCA Organizational Unit Name (eg, section) []:Security Common Name (e.g. server FQDN or YOUR name) []:companyborsomeca.com Email Address []:root@companyborsomeca.com <pre> Once this is done we are going to issue a Certificate Signing Request for our server . This process is the same you would do if you want to purchase a valid SSL certificate, it also needs a key before generation of csr file. <strong>Creating our joke server ssl key</strong> <pre title="Creating the key for our joke app certificate" lang="ssh"> openssl genrsa -out jokes.mycodingpains.com.key Generating RSA private key, 2048 bit long modulus ..+++ ...................................+++ e is 65537 (0x10001) |
Notice here as well, I have taken out the request for pass phrase
Creating our joke server certificate signing request
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
openssl req -new -key jokes.mycodingpains.com.key -out jokes.mycodingpains.com.csr You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:GH State or Province Name (full name) [Some-State]:Greater Accra Locality Name (eg, city) []:Accra Organization Name (eg, company) [Internet Widgits Pty Ltd]:Mycodingpains.com Organizational Unit Name (eg, section) []:Jokes Unit Common Name (e.g. server FQDN or YOUR name) []:jokes.mycodingpains.com Email Address []:joseph@mycodingpains.com Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: |
We can now proceed with our signing of the certificate. If we were to buy digicert or verisign etc certificate, we would have submitted this CSR to be signed and a Certificate sent to us back. But we are our Own Certificate Authority or should I say CompanyB is our mycodinpains server CA.
Signing the server certificate with CompanyBorSomeCA
1 2 3 4 5 |
openssl x509 -req -days 365 -in jokes.mycodingpains.com.csr -CA CompanyBorSomeCA.crt -CAkey CompanyBorSomeCA.key -set_serial 01 -out jokes.mycodingpains.com.crt Signature ok subject=/C=GH/ST=Greater Accra/L=Accra/O=Mycodingpains.com/OU=Jokes Unit/CN=jokes.mycodingpains.com/emailAddress=joseph@mycodingpains.com Getting CA Private Key Enter pass phrase for CompanyBorSomeCA.key: |
Running our App
Now that we have all our requirements done, let’s upload our spring boot application . We will need to also upload some certificates. CompanyACA.crt, jokes.mycodingpains.com.crt, jokes.mycodingpaons.com.key and install nginx server
1 2 3 4 |
scp -i someaccesskey.pem chucknorrisdb-0.0.1-SNAPSHOT.jar joseph@someip:/home/joseph scp -i someaccesskey.pem /path/to/CompanyACA.crt joseph@someip:/home/joseph scp -i someaccesskey.pem /path/to/jokes.mycodingpains.com.crt joseph@someip:/home/joseph scp -i someaccesskey.pem /path/to/jokes.mycodingpaons.com.key joseph@someip:/home/joseph |
1 |
sudo apt-get install nginx |
Let’s install java in order to run our app courtesy of webupd8 group
1 2 3 |
sudo add-apt-repository ppa:webupd8team/java sudo apt-get update sudo apt-get install oracle-java8-installer |
Running our app is then simple
1 |
java -jar chucknorrisdb-0.0.1-SNAPSHOT.jar |
Let’s create our virtual host for running our jokes app
1 |
sudo nano /etc/nginx/sites-available/jokes.mycodinpains.com.conf |
Let’s have its content similar to the configuration below, that’s depending on name used during the tutorial and the domain name used
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#/etc/nginx/sites-available/jokes.mycodinpains.com.conf server { listen 443 ssl; server_name jokes.mycodingpains.com; ssl_certificate /srv/ssl/jokes.mycodingpains.com/jokes.mycodingpains.com.crt; ssl_certificate_key /srv/ssl/jokes.mycodingpains.com/jokes.mycodingpains.com.key; #ssl_client_certificate /srv/ssl/jokes.mycodingpains.com/CompanyACA.crt; #ssl_verify_client on; #ssl_verify_depth 1; ssl_prefer_server_ciphers on; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:ECDH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!eNULL:!MD5:!DSS:!EXP:!ADH:!LOW:!MEDIUM; access_log /var/log/nginx/ssl_jokes_mycodingpains_com_access.log; error_log /var/log/nginx/ssl_jokes_mycodingpains_com_error.log debug;# this can be warn location ~* \.(jpg|jpeg|gif|css|png|js|ico)$ { access_log off; expires max; } location / { proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_pass http://localhost:8080/; } } |
Let’s enable our virtual host and reload the config
1 2 3 4 5 6 7 8 |
cd /etc/nginx/sites-enable sudo ln -s /etc/nginx/sites-available/jokes.mycodinpains.com.conf jokes.mycodinpains.com.conf sudo nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful sudo service nginx restart * Restarting nginx nginx [ OK ] |
Notice: mutual ssl certificate has been disable on line 12,13,14 . This allows us to test our setup first
Testing With one way SSL
Browser
The image above shows normal spring boot error page . This means our ssl is working. Let’s try make some request to our jokes app using http requester
cURL
Let’s make further call using curl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
curl -k --data "count=3" https://jokes.mycodingpains.com/jokes { "status":"Success", "message":"", "response":[ { "id":"340", "joke":"A man once claimed Chuck Norris kicked his ass twice, but it was promptly dismissed as false - no one could survive it the first time.", "categories":[] }, { "id":"497", "joke":"If Chuck Norris writes code with bugs, the bugs fix themselves.", "categories":["nerdy"] }, { "id":"281", "joke":"4 out of 5 doctors fail to recommend Chuck Norris as a solution to most problems. Also, 80% of doctors die unexplained, needlessly brutal deaths.", "categories":[] } ] } |
Testing with 2 way SSL without Client certificate
Browser
Let’s remove the comment from the configuration at /etc/nginx/sites-available/jokes.mycodinpains.com.conf and restart the nginx server.
Our request was rejected because our browser did not present the adequate client’s certificate to server. Same thing happens with curl request below:
cURL
1 2 3 4 5 6 7 8 9 |
curl -k --data "count=4" https://jokes.mycodingpains.com/jokes <html> <head><title>400 No required SSL certificate was sent</title></head> <body bgcolor="white"> <center><h1>400 Bad Request</h1></center> <center>No required SSL certificate was sent</center> <hr><center>nginx/1.4.6 (Ubuntu)</center> </body> </html> |
Testing with 2 way SSL with Client certificate
Browser
In order to test this on a browser, we need a PKCS12 format of our certificate. Let’s then create a pfx format of our certificate
1 2 3 |
openssl pkcs12 -export -out nejifromcompanyA.pfx -inkey nejifromcompanyA.key -in nejifromcompanyA.crt Enter Export Password: Verifying - Enter Export Password: |
I used 1234 for the export password. That’s the password we will use when adding our pfx to our browser
I am using Mozilla Firefox on a linux machine so preference it at : Edit > Preferences > Advanced > View Certificate > select “Your Certificate” > click on Import > choose your pfx file
On the step below, I have used the code : 1234 used during export of pkcs12 (nejifromcompanyA.pfx) file
With that we make a call to the browser and it selects the needed client certificate and all we need to do is to confirm.
cURL
Let’s now select the client’s certificate to make call with.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
curl -k --cert nejifromcompanyA.crt --key nejifromcompanyA.key --data "count=4" https://jokes.mycodingpains.com/jokes { "status":"Success", "message":"", "response": [ { "id":"473", "joke":"Chuck Norris can overflow your stack just by looking at it.", "categories":["nerdy"] }, { "id":"236", "joke":"Sticks and stones may break your bones, but a Chuck Norris glare will liquefy your kidneys.", "categories":[] }, { "id":"288", "joke":"To be or not to be? That is the question. The answer? Chuck Norris.", "categories":[] }, { "id":"458", "joke":"Chuck Norris can write infinite recursion functions and have them return.", "categories":["nerdy"] } ] } |
JVM client
I have fallen in love with groovy for a while now and I will rather use groovy to make a non exhaustive client taking advantage of the scripting side of the language. Here again, we need to do a couple of concerns. Java by nature will throw an exception of the type “javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated” if the server ssl is self signed or has expired. We will take care of that 😐
We need to tell our groovy client to chill and trust our jokes.mycodingpains.com.crt by creating a truststore and adding jokes server certificate to it.
The command below will create a truststore for us of the name jokesapp.jks and will add our jokes server certificate with some key and store password “jokes1234”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
keytool -import -v -trustcacerts -alias jokesapp -file jokes.mycodingpains.com.crt -keystore jokesapp.jks -keypass jokes1234 -storepass jokes1234 Owner: EMAILADDRESS=joseph@mycodingpains.com, CN=jokes.mycodingpains.com, OU=Jokes Unit, O=Mycodingpains.com, L=Accra, ST=Greater Accra, C=GH Issuer: EMAILADDRESS=root@companyborsomeca.com, CN=companyborsomeca.com, OU=Security, O=CompanyBorSomeCA, L=Accra, ST=Greater Accra, C=GH Serial number: 1 Valid from: Sun Sep 18 22:36:19 GMT 2016 until: Mon Sep 18 22:36:19 GMT 2017 Certificate fingerprints: MD5: F6:87:A7:FF:2E:6A:68:C4:55:A5:50:52:03:6A:CA:B8 SHA1: F1:35:07:A0:C2:A2:7E:AC:6F:80:6B:20:AC:A8:E1:23:3D:39:96:0A SHA256: E7:6E:FC:C3:DA:23:72:AD:38:1A:F5:82:BA:AA:77:B2:90:EE:A4:82:5E:DE:95:DE:1C:6F:20:22:03:D2:0B:90 Signature algorithm name: SHA256withRSA Version: 1 Trust this certificate? [no]: yes Certificate was added to keystore [Storing jokesapp.jks] |
Now we will need to take care of our client certificate bit. For that, we will need to create a keystore this time. our keystore will contain both our nejifromcompanyA.crt and nejifromcompanyA.key. This is exactly what nejifromcompanyA.pfx represent. Remember we generated the pfx file for our browser to be able to authenticate with our jokes server. The pass phrase we used for the export of the pfx file is 1234. The command below will use the pfx and create a jks file out of it. I will ask for the export pass phrase of the pfx file as it’s source password and will ask for us to type in a new pass phrase as destination password. We will use neji1234 for the destination password
1 2 3 4 5 6 |
keytool -importkeystore -srckeystore nejifromcompanyA.pfx -srcstoretype pkcs12 -destkeystore nejiclientstore.jks -deststoretype JKS Enter destination keystore password: Re-enter new password: Enter source keystore password: Entry for alias 1 successfully imported. Import command completed: 1 entries successfully imported, 0 entries failed or cancelled |
Let’s do a routine verification of our truststore and keystore just to be on the safer side.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
keytool -list -v -keystore jokesapp.jks Enter keystore password: Keystore type: JKS Keystore provider: SUN Your keystore contains 1 entry Alias name: jokesapp Creation date: Sep 20, 2016 Entry type: trustedCertEntry Owner: EMAILADDRESS=joseph@mycodingpains.com, CN=jokes.mycodingpains.com, OU=Jokes Unit, O=Mycodingpains.com, L=Accra, ST=Greater Accra, C=GH Issuer: EMAILADDRESS=root@companyborsomeca.com, CN=companyborsomeca.com, OU=Security, O=CompanyBorSomeCA, L=Accra, ST=Greater Accra, C=GH Serial number: 1 Valid from: Sun Sep 18 22:36:19 GMT 2016 until: Mon Sep 18 22:36:19 GMT 2017 Certificate fingerprints: MD5: F6:87:A7:FF:2E:6A:68:C4:55:A5:50:52:03:6A:CA:B8 SHA1: F1:35:07:A0:C2:A2:7E:AC:6F:80:6B:20:AC:A8:E1:23:3D:39:96:0A SHA256: E7:6E:FC:C3:DA:23:72:AD:38:1A:F5:82:BA:AA:77:B2:90:EE:A4:82:5E:DE:95:DE:1C:6F:20:22:03:D2:0B:90 Signature algorithm name: SHA256withRSA Version: 1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
keytool -list -v -keystore nejiclientstore.jks Enter keystore password: Keystore type: JKS Keystore provider: SUN Your keystore contains 1 entry Alias name: 1 Creation date: Sep 20, 2016 Entry type: PrivateKeyEntry Certificate chain length: 1 Certificate[1]: Owner: EMAILADDRESS=neji@companya.com, CN=neji.companya.com, OU=Hyuga Clan, O=CompanyA, L=Accra, ST=Greater Accra, C=GH Issuer: EMAILADDRESS=security@companya.com, CN=root.companya.com, OU=CompanyA DevOps, O=CompanyA, L=Accra, ST=Greater Accra, C=GH Serial number: 1 Valid from: Sun Sep 18 22:03:35 GMT 2016 until: Mon Sep 18 22:03:35 GMT 2017 Certificate fingerprints: MD5: B8:7D:E3:FC:E4:78:16:08:84:3C:54:8C:12:FD:79:53 SHA1: A2:93:A1:03:88:CF:A1:84:87:41:4E:7C:71:5E:84:95:88:1A:01:1A SHA256: 78:34:66:D7:D9:0E:CF:2D:D2:31:6A:95:9C:53:81:02:88:48:3C:BA:14:51:95:8B:0E:72:C1:99:1B:31:1E:76 Signature algorithm name: SHA256withRSA Version: 1 |
It looks like it’s all nice and clean, let’s create our groovy client then.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@Grab('org.codehaus.groovy.modules.http-builder:http-builder:0.7.1') import groovyx.net.http.HTTPBuilder import static groovyx.net.http.ContentType.* import static groovyx.net.http.Method.POST import groovyx.net.http.RESTClient import java.security.KeyStore import org.apache.http.conn.scheme.Scheme import org.apache.http.conn.ssl.SSLSocketFactory def trustStore = KeyStore.getInstance("JKS") getClass().getResource("./jokesapp.jks").withInputStream { nejijks -> trustStore.load(nejijks, "jokes1234".toCharArray()) } def keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) getClass().getResource("./nejiclientstore.jks").withInputStream { nejiclient -> keyStore.load(nejiclient, "neji1234".toCharArray()) } def http = new HTTPBuilder("https://jokes.mycodingpains.com") http.client.connectionManager.schemeRegistry.register( new Scheme("https", 443,new SSLSocketFactory(keyStore,"1234",trustStore)) ) def jokes = http.request(POST){ uri.path= "/jokes" send URLENC, [count: 4] response.success = { resp, json -> println json } } |
Notice this script is in the same directory as all the certificate files.
Calling our server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
groovy calltojokesatcodingpains.groovy [ message:, response:[ [ categories:[], id:123, joke:Some people wear Superman pajamas. Superman wears Chuck Norris pajamas. ], [ categories:[], id:43, joke:Police label anyone attacking Chuck Norris as a Code 45-11.... A suicide. ], [ categories:[], id:355, joke:In the X-Men movies, none of the X-Men super-powers are done with special effects. Chuck Norris is the stuntman for every character. ], [ categories:[], id:347, joke:Some people ask for a Kleenex when they sneeze, Chuck Norris asks for a body bag. ] ], status:Success ] |
That’s all folks, I hope this saves you some time and from some heavy google-fu and banging of head. This post was influenced by Nate Good’s post But there were a lot reading and head banging especially during some integration with partners from South Africa. Maybe will invest some time in making this work with java straight.