Did you know that x509 certificates, the certificates that webservers use to prove their identity during the establishment of an HTTPS connection, can also be used by a client (like your webbrowser) to prove its identity, and even to authenticate?
I'm talking here about so-called client certificate authentication.
Client certificate authentication is especially popular in environments with high security requirements. They can even be used to enforce 2-factor authentication, if in addition to a client certificate you also require a password. That usecase is however out of scope for this blog post.
For Converse.js, you'll need at least version 2.0.0.
Here's what it looke like:
The technical details and background
XMPP and SASL
XMPP supports authentication with client certificates, because it uses SASL (Simple Authentication and Security Layer).
SASL provides an abstraction that decouples authentication mechanisms from application protocols.
This means that XMPP developers don't need to know about the implementation details of any authentication mechanisms, as long as they conform to SASL.
Up til version 1.2.7, Strophe.js supported the SASL auth mechanisms: ANONYMOUS, OAUTHBEARER, SCRAM-SHA1, DIGEST-MD5 and PLAIN.
For client certificate auth, we need another SASL mechanism, namely EXTERNAL. What EXTERNAL means, is that authentication happens externally, outside of the protocol layer. And this is exactly what happens in the case of client certificates, where authentication happens not in the XMPP layer, but in the SSL/TLS layer.
Strophe.js version 1.2.8 now supports SASL-EXTERNAL, which is why client certificate authentication now also works.
How do you communicate with an XMPP server from a web-browser?
There are two ways that you can communicate with an XMPP server from a web-browser (e.g. from a webchat client such as Converse.js).
- You can use XMLHttpRequests and BOSH, which you can think of as an XMPP-over-HTTP specification.
- You can use websockets.
Both of these protocols, HTTP and websocket, have secure SSL-reliant versions (HTTPS and WSS), and therefore in both cases client certificate authentication should be possible, as long as the server requests a certificate from the client.
I'm going to focus on BOSH and HTTPS, since this was my usecase.
The HTTPS protocol makes provision for the case where the server might request a certificate from the client.
NOTE: Currently the only XMPP server that supports client certificate authentication with BOSH is Openfire, and funnily enough, only Openfire 3. In Openfire 4, they refactored the certificate handling code and broke client certificate authentication with BOSH. I've submitted a ticket for this to their tracker: https://issues.igniterealtime.org/browse/OF-1191
The authentication flow
So this is how the authentication flow works. I'll illustrate how the authentication flow works by using actual log output from converse.js
NOTE: My XMPP server's domain is called debian, because I was running it on a Debian server and because naming things is hard. In hindsight, this wasn't a good name since it might confuse the dear reader (that means you).
2016-09-15 12:07:05.481 converse-core.js:128 Status changed to: CONNECTING
Firstly, Converse.js sends out a BOSH stanza to the XMPP server debian, to establish a new BOSH session.
2016-09-15 12:07:05.482 converse-core.js:128 <body rid="1421604076" xmlns="http://jabber.org/protocol/httpbind" to="debian" xml:lang="en" wait="60" hold="1" content="text/xml; charset=utf-8" ver="1.6" xmpp:version="1.0" xmlns:xmpp="urn:xmpp:xbosh"/> 2016-09-15 12:07:06.040 bosh.js:749 XHR finished loading: POST "https://debian:7445/http-bind/"
The above stanza was sent as an XMLHttpRequest POST, and the above XML was sent as the Request Payload.
Strophe.js takes care of all this, so nothing to worry about, but sometimes digging through the internals is fun right? Right?!
2016-09-15 12:07:06.042 converse-core.js:128 <body xmlns="http://jabber.org/protocol/httpbind" xmlns:stream="http://etherx.jabber.org/streams" from="debian" authid="fe0ee6ab" sid="fe0ee6ab" secure="true" requests="2" inactivity="30" polling="5" wait="60" hold="1" ack="1421604076" maxpause="300" ver="1.6"> <stream:features> <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"> <mechanism>EXTERNAL</mechanism> </mechanisms> <register xmlns="http://jabber.org/features/iq-register"/> <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/> <session xmlns="urn:ietf:params:xml:ns:xmpp-session"> <optional/> </session> </stream:features> </body>
So now the XMPP server, debian, has responded, and it provides a list of SASL mechanisms that it supports. In this case it only supports EXTERNAL.
Luckily our webchat client supports SASL-EXTERNAL, so it responds in turn and asks to be authenticated.
2016-09-15 12:07:06.147 converse-core.js:128 <body rid="1421604077" xmlns="http://jabber.org/protocol/httpbind" sid="fe0ee6ab"> <auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="EXTERNAL">dXNlcjAxQGRlYmlhbg==</auth> </body>
Now here comes the tricky part. The XMPP server's BOSH servlet, asks the webbrowser (which is establishing the HTTPS connection on our behalf) to give it the client certificate for this user.
The webbrowser will now prompt the user to choose the right client certificate. Once this is done, the XMPP server authenticates the user based upon this certificate.
2016-09-15 12:07:06.177 bosh.js:749 XHR finished loading: POST 2016-09-15 12:07:06.180 converse-core.js:128 <body xmlns="http://jabber.org/protocol/httpbind" ack="1421604077"> <success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/> </body>
The XMPP server responds with success and we're logged in!
How to set up client certificate authentication with Converse.js and OpenFire 3.10.3
NOTE: Thanks goes out to Dennis Shtemberg from Infusion, who initially tested client certificate authentication with BOSH on Openfire and on whose notes the following is based.
1. Install Openfire 3.10.3
On Debian(-based) Linux, you can simply do the following:
wget http://www.igniterealtime.org/downloadServlet?filename=openfire/openfire_3.10.3_all.deb sudo dpkg -i openfire_3.10.3_all.deb
2. Configure Openfire's system properties
Open the admin console: http://localhost:9090/ (where localhost is the host the server is running on)
Navigate to Server > Server Manager > System Properties and add the following properties:
Property Value xmpp.client.cert.policy needed xmpp.client.certificate.accept-selfsigned true xmpp.client.certificate.verify true xmpp.client.certificate.verify.chain true xmpp.client.certificate.verify.root true sasl.mechs EXTERNAL
Make sure the xmpp.domain value is set to the correct host. If you're running Openfire on localhost, then you need to set it to localhost. If you're not using localhost, then replace all mention of localhost below with the xmpp.domain value.
3. Lay the groundwork for generating an SSL client certificate
First, make sure you have OpenSSL installed: aptitude install openssl Then create a directory for certificate files: mkdir ~/certs
Now create a config file called user01.cnf (~/certs/user01.cnf) with the following contents:
[req] x509_extensions = v3_extensions req_extensions = v3_extensions distinguished_name = distinguished_name [v3_extensions] extendedKeyUsage = clientAuth keyUsage = digitalSignature,keyEncipherment basicConstraints = CA:FALSE subjectAltName = @subject_alternative_name [subject_alternative_name] otherName.0 = 22.214.171.124.126.96.36.199.5;UTF8:user01@localhost [distinguished_name] commonName = user01@localhost
The otherName.0 value under subject_alternative_name assigns the user's JID to an ASN.1 Object Identifier of "id-on-xmppAddr". The XMPP server will check this value to figure out what the JID is of the user who is trying to authenticate.
For more info on the id-on-xmppAddr attribute, read XEP-178.
4. Generate an SSL client certificate
Generate a self-signed, leaf SSL certificate, which will be used for client authentication.
Generate a private RSA key
openssl genrsa -out user01.key 4096
Generate a sigining request:
openssl req -key user01.key -new -out user01.req -config user01.cnf -extensions v3_extensions
- when prompted for a DN enter: user01@localhost
Generate a certificate by signing user01.req
openssl x509 -req -days 365 -in user01.req -signkey user01.key -out user01.crt -extfile user01.cnf -extensions v3_extensions
Generate PKCS12 formatted certificate file, containing the private key and the certificate. This will be the client certificate which you will log in with.
openssl pkcs12 -export -inkey user01.key -in user01.crt -out user01.pfx -name user01
- when prompted for export password enter: user01
5. Install the PKCS12 certificate on your local machine
Double click the pfx file and follow the steps to import it into your machine's keystore.
6. Import the x509 certificate into Openfire
sudo keytool -importcert -keystore /etc/openfire/security/truststore -alias user01 -file ~/certs/user01.crt sudo keytool -importcert -keystore /etc/openfire/security/client.truststore -alias user01 -file ~/certs/user01.crt sudo systemctl restart openfire
NOTE: The default keystore password is "changeit"
7. Create the user associated with the SSL client certificate
Go back to Openfire admin console, navigate to Users/Groups > Create New User and create a new user.
- Username: user01
- Password: user01 (This is not controlled by Openfire).
- Click Create User
8. (When using Java 1.7) Patch Openfire
When trying to log in, I received the following error:
2016.09.08 00:28:20 org.jivesoftware.util.CertificateManager - Unkown exception while validating certificate chain: Index: 0, Size: 0
Turns out the likely cause for this is the fact that I was using the outdated Java version 1.7.
At the time, I didn't know that Java is the culprit, so I patched the following code
If you read the comments in the link above, you'll see there are two sections, with one being outcommented. I swopped out the two sections, and then recompiled Openfire.
After that, client certificate auth worked. The best way to avoid doing this is apparently to just use Java 1.8.
9. Test login with converse.js.
Now you're done with setting up Openfire and you can test logging in with Converse.js.
Download the latest version of Converse.js from the releases page.
To hide the password field (since the password won't be checked for anyway), you need to open index.html in your text editor and add authentication: 'external to the converse.initialize call.
Then open index.html in your browser.
In the converse.js login box, type the JID of the user, e.g. user01@localhost and click login.
NOTE: If things go wrong, pass debug: true to converse.initialize, then open your browser's developer console and check the output. Check especially the XHR calls to http-bind. Checking the output in the Network tab can also be very helpful. There you'll see what Openfire responds to requests to its BOSH URL.
Client certificate authentication is a bit of a niche requirement, doing so with BOSH/HTTP even more so.
However, I expect webchat XMPP clients to become more and more prevalent in the coming years, even on the desktop, for example when packaged with Github's Electron (an Electron version of converse.js is planned BTW, based on the fullscreen version inverse.js).
The fact that this works because of SASL-EXTERNAL authentication being added to Strophe.js means that this functionality is not only possible in Converse.js, but all webchat clients built on Strophe.js (granted that they use version 1.2.8 or higher).
Unfortunately XMPP server support is lacking, with only Openfire supporting this usecase currently, and not yet (at the time of writing) in the 4.0.x branch. To see whether this gets fixed, keep an eye on the relevant ticket