HTTPS and Java – Pitfalls and Best Practices
As software developers, we often want to talk to services over the internet, usually using HTTP. However, it’s now very common to see online services using HTTPS – an extension of HTTP which enables secure communications over a network.
This move has made life more interesting for developers who want to interact with these services. Most of the time, connecting to a host over HTTPS “just works” from Java, but sometimes things don’t quite work … and Java can be a bit cryptic in how it reports the failure.
To be more clear, HTTPS provides three security-related properties: Authentication, Data Integrity, and Encryption. In this post, we will look closer at two of the properties of HTTPS – Authentication and Encryption. We will also look at some of the pitfalls of HTTPS we can encounter as software developers, and how to solve them the right way.
Let’s say we’re writing a Java service which calls an online service over HTTPS. How about this for an error message:
PKIX path building failed
What does that even mean? Or how about this one:
Received fatal alert: handshake_failure
It’s the worst kind of scenario: something which “ought to be simple” doesn’t work, and you don’t know why.
It might feel tempting at this point to work around the problem by disabling some validation, or falling back to plain old HTTP. But this would remove a layer of security – an irresponsible approach. So instead let’s learn all about HTTPS and Java so we can deal with these problems correctly when they occur.
But wait … why did HTTPS become so much of a thing all of a sudden?
There has been a growing movement for online services to use HTTPS everywhere. For example, this talk at the Google I/O conference in 2014 gives a strong case for why “all communications should be secure by default”, not just traditionally sensitive services such as online banking.
In 2016, the Google Chrome web browser began to flag sites which collect form inputs as “Insecure” if they did not use HTTPS. Things rolled on until July 2018, when the Google Chrome web browser started to flag all websites which were not secured by HTTPS as “Not Secure”.
Nobody wants their website to look like this, so there was a strong incentive for online services to use HTTPS in an effort to not appear insecure to their users.
To enable HTTPS, a website owner would need to obtain a certificate – sometimes known in this context as a HTTPS, TLS, or SSL certificate. Remember that one of the properties of HTTPS is Authentication. In this context, authentication means the ability to prove ownership of the domain. A certificate is issued by a Certificate Authority (or CA). One responsibility of a CA is to ensure the identity of a certificate holder to ensure they are genuinely in control of the domain the certificate is issued for.
There are many Certificate Authorities available for use. One example of a fairly new Certificate Authority is Let’s Encrypt. Due to it being newly created, older versions of Java were not able to connect to hosts which used a Let’s Encrypt certificate. Java would fail to connect to the host, with the cryptic “PKIX path building failed” message which we saw earlier.
But what does this really mean? To find out, let’s learn a little more about certificates and the chain of trust.
Chain of Trust
When a Java program connects to a host over HTTPS, it’s important to know you’re really communicating with who you think you are. If I write a program which connects to https://www.rightmove.co.uk, then I’m sending information over a secure channel. How can I be sure I’m not talking to a malicious third party who is somehow intercepting my traffic, such as user credentials?
The identity of a server is proven with certificates. As an overview, when a Java program connects to a host over HTTPS, before secure communication can take place, the server sends back its certificate. Java then verifies that this certificate was issued by a legitimate Certificate Authority (CA). This is possible because Java ships with a trust store, containing a white list of trusted Certificate Authorities. As Java updates are rolled out, from time to time certificates are added to (or removed from) this trust store.
It was not possible for Java to communicate over HTTPS with sites certified by Let’s Encrypt until Java 8 update 101, which was released in July 2016. By this point the public beta was complete, and Let’s Encrypt had issued about 5 million certificates. If you try to connect to one of these websites using an earlier version of Java, you will see a stack trace containing that message “PKIX path building failed”. This means Java is unable to validate the certificate chain, because it doesn’t have the issuing CA in its trust store.
So what do we really mean when we say “certificate chain”? In practice, there are several levels of Certificate Authority: Intermediaries, and Root CAs. There is a small number of Root Certificate Authorities. The role of these is to hold a very closely-guarded private key, in a very secure location such as a HSM. Each private key is used to generate a certificate, representing the Root CA. This certificate contains a public key which can be used to verify the holder of the certificate also holds the corresponding private key. These certificates are installed as standard into web browsers, operating systems, and Java installations. The presence of this public key in Java’s trust store means it is able to verify that a certificate presented by a server was issued by one of these Root Certificate Authorities.
Certificates for individual websites are issued by Intermediary CAs. These intermediary Certificate Authorities are issued certificates by a Root CA – for example Let’s Encrypt is certified by the root CA called IdenTrust. The reason for the existence of these intermediaries is that it’s relatively easy to create new intermediate certificates (and also to revoke them), without having to roll out a software update en-masse each time. These intermediaries can then issue certificates to individual sites. Client software will validate the chain of certificates – your own domain can be certified by Let’s Encrypt, and Let’s Encrypt is certified by the Root CA called IdenTrust.
In the case of IdenTrust, their certificate was only added to Java 8 Update 101. This meant that any versions of Java prior to this version would fail to connect to any sites certified by Let’s Encrypt.
So to recap, keeping up to date with Java releases is really important to ensure you can safely authenticate parties, as well as maintaining compatibility with your customers.
So far, we’ve looked at how certificates can support the Authentication property of HTTPS. The certificate also enables a second property of HTTPS – Encryption of the traffic. This encryption is possible because the certificate contains a public key which allows clients to encrypt data, so only those holding the corresponding private key (the website owner) will be able to decrypt.
This encryption is achieved using ciphers. There may be a range of ciphers available on both ends of the connection, and the cipher chosen for the communication will be agreed in the initial TLS handshake. If no cipher can be agreed upon – say if the web server wants to use a strong, modern cipher such as AES 256, and Java cannot support that cipher, then Java will fail to connect with the message we saw earlier: “Received fatal alert: handshake_failure”.
AES (Advanced Encryption Standard) is considered a highly secure cipher – it’s good enough to be approved for Top Secret material held by the National Security Agency in the USA. AES is available in several strengths, including 128 bit and 256 bit which is the strongest. Before 2018, 128 bit AES encryption was available in Java, but if you wanted the stronger 256 bit version, you’d need to install the Unlimited Strength Jurisdiction Policy files. The reason for this was that historically, the USA controlled the export of strong encryption technology very carefully. 256 bit AES is now available in Java without any additional installation, but only since an update to Java released around January 2018.
So if you’re running a version of Java which does not have AES 256 available and enabled, then it cannot be used as a cipher to encrypt traffic. Depending on the web server configuration, this will either result in the “handshake failure” error message, or communications may fall back to a weaker cipher.
As well as adding stronger ciphers, Java updates also remove support for old ciphers which are known to be insecure. One example of this is RC4, which has been around for a while (it was first designed in 1987). This cipher is now known to have multiple vulnerabilities, and has been disallowed by updates to the TLS specification since 2015. RC4 was removed in Java 8 Update 60, so any versions of Java which still support RC4 can be considered insecure in this context. So, if you are using a version of Java earlier than this, then it’s possible HTTPS traffic could use this weak cipher and be vulnerable.
So keep your Java installations up to date, including minor releases, to keep up to date with security standards.
Perfect Forward Secrecy
At the present time, with the possibility of data breaches and digital eavesdropping, privacy is an important subject. What’s to prevent a malicious third-party from collecting traffic going to or from a website?
It might seem adequate to use HTTPS to encrypt traffic to a website, meaning it’s impossible to decrypt the traffic unless you also have access to the private key. But what if a malicious actor collects encrypted traffic over a long period of time? They may not be able to decrypt the traffic just yet … but what if at a later date they recover the private key which secures the website? Would they be able to decrypt all the collected traffic?
To answer this question, we need to look a little deeper at the TLS (Transport Layer Security) protocol, which is used to set up a secure communication channel to a HTTPS website.
Communication with a HTTPS website is encrypted in part with a short-lived, shared secret, known to both parties. This shared secret is derived from the long-living private key which is issued to a website when it is issued with a certificate.
Here’s a short example of how encrypted traffic could be recovered at a later date. If a connection to a HTTPS website uses the RSA key exchange algorithm (now considered insecure), then it is vulnerable to this scenario. When the encrypted session is set up, the client (browser or Java program) decides the shared secret. It encrypts this value using the server’s public key (which is present in the certificate). This encrypted shared secret is then sent to the server, and future secret communications are based on that. So, if the private key of the server was recovered by a malicious third party, then past and future traffic could be decrypted by that party.
To prevent this scenario, key exchange can be carried out with a property known as Perfect Forward Secrecy. This means that communications are encrypted by a one-time, ephemeral session key which exists only for that session. So traffic cannot be decrypted at a later date unless you have the one-time key for that particular session.
Whether Perfect Forward Secrecy is possible depends on the key exchange algorithm selected between the client and server, as part of the TLS handshake. There have been a number of versions of TLS (1.0 to 1.3), each version improving security. The most recent version (1.3) entirely removes support for key exchange algorithms which do not support Perfect Forward Secrecy, such as RSA. Support for TLS 1.3 was added in Java 11, so it can be said that Java 11 can ensure Perfect Forward Secrecy in secure communications, as long as the web server also supports TLS 1.3.
In closing, when developing software, security and privacy should be a high priority. So keep your Java installations up to date!
Don’t be tempted to work around security-related errors when they occur. Instead, understand what they really mean and fix any underlying problems which compromise security.