1. Overview
Digital certificates are important in establishing trusted and secure online communication. We often use them to ensure the data exchanged between the client and the web server remains secure.
In this tutorial, we’ll explore how to determine in Java whether a given certificate is self-signed or signed by a trusted Certificate Authority (CA).
However, due to the diversity of certificates and security concepts, there is no one-size-fits-all solution. We often need to choose the best approach for our specific context and requirements.
2. Self-Signed vs. CA-Signed Certificate
First, let’s examine the differences between self-signed and CA-signed certificates.
Simply put, a self-signed certificate is generated and signed by the same entity. Even though it provides encryption, it doesn’t verify trust by an independent authority. In other words, it doesn’t involve any third-party Certificate Authority (CA).
Consequently, when a user’s web browser encounters a self-signed certificate, it may issue a security warning since the certificate’s authenticity can’t be independently verified.
We often use them in private networks and for testing purposes.
On the other hand, CA-signed certificates are signed by trusted Certificate Authorities. The majority of web browsers and operating systems recognize and accept these CAs.
Additionally, the CA-signed certificate proves the entity holding the certificate is the legitimate owner of the domain, helping users trust they’re communicating with the genuine server and not an intermediary.
Now, let’s see how to check whether we’re dealing with a self-signed or CA-signed certificate using Java.
3. Checking if the Certificate Is Self-Signed
Before we start, let’s generate the certificate we’ll use throughout our examples. The easiest way to generate a self-signed certificate is with the keytool tool:
keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -validity 365 -keysize 2048
Here, we created a self-signed certificate with the selfsigned alias. Furthermore, we stored it inside the keysore.jks keystore.
3.1. Comparing Issuer and Subject Values
As previously mentioned, the same entity generates and signs a self-signed certificate.
The issuer part of the certificate represents the certificate’s signer. The self-signed certificate has the same value for the subject (Issued To) and the issuer (Issued By). To put it differently, to determine whether we’re dealing with a self-signed certificate, we’ll compare its subject and issuer information.
Furthermore, Java API provides the java.security.cert.X509Certificate class for dealing with certificates. With this class, we can interact with X.509 certificates and perform various checks and validations.
Let’s check whether the subject and issuer match. We can achieve this by extracting the relevant fields from the X509Certificate object and checking if they match:
@Test
void whenCertificateIsSelfSigned_thenSubjectIsEqualToIssuer() throws Exception {
X509Certificate certificate = (X509Certificate) keyStore.getCertificate("selfsigned");
assertEquals(certificate.getSubjectDN(), certificate.getIssuerDN());
}
3.2. Verifying the Signature
Another way we can check if we’re dealing with a self-signed certificate is to verify it using its own public key.
Let’s check the self-signed certificate using the verify() method:
@Test
void whenCertificateIsSelfSigned_thenItCanBeVerifiedWithItsOwnPublicKey() throws Exception {
X509Certificate certificate = (X509Certificate) keyStore.getCertificate("selfsigned");
assertDoesNotThrow(() -> certificate.verify(certificate.getPublicKey()));
}
However, if we pass the CA-signed certificate, the method would throw an exception.
4. Checking if the Certificate Is CA-Signed
To qualify as a CA-signed, a certificate must be part of a chain of trust leading back to a trusted root CA. Simply put, a certificate chain contains a list of certificates starting from the root certificate and ending with the user’s certificate. Each certificate in the chain signs the next certificate.
When we talk about the chain of trust, there are different certificate types:
- Root Certificate
- Intermediate Certificate
- End Entity Certificate
Furthermore, we use the root and intermediate certificates of the hierarchy to issue and verify end entity certificates.
For the purposes of this tutorial, we’ll use the certificate obtained from the Baeldung site:
The complexity of checking CA-signed certificates increases if the end entity certificate is part of a certificate chain. In such a scenario, we might need to examine the entire chain to determine if we have a CA-signed certificate. The issuer of the certificate might not be the root CA directly but rather an intermediate CA that signed with the root CA.
Now, if we examine the certificate hierarchy, we can see the certificate is part of a certificate chain:
- Baltimore CyberTrust Root – Root CA
- Cloudflare Inc ECC CA-3 – Intermediate CA
- sni.cloudflaressl.com – End Entity (used on the Baeldung site)
4.1. Using Truststore
We can create our own truststore to check if one of the certificates we trust signed the end entity certificate.
When setting up a truststore, we typically include the root CA certificates as well as any intermediate CA certificates required to build the chain of trust. This way, our application can effectively validate certificates presented by other parties.
An advantage of using a truststore is the ability to decide which CA certificates we trust and which we don’t.
From our example, the Baltimore CyberTrust Root certificate signed the intermediate Cloudflare certificate, which signed our end entity certificate.
Now, let’s add both to our truststore:
keytool -importcert -file cloudflare.cer -keystore truststore.jks -alias cloudflare
keytool -importcert -file root.cer -keystore truststore.jks -alias root
Next, to check if we trust the given end entity certificate, we need to find a way to get the root certificate. Let’s create the getRootCertificate() method that searches for a root certificate:
public static X509Certificate getRootCertificate(X509Certificate endEntityCertificate, KeyStore trustStore)
throws Exception {
X509Certificate issuerCertificate = findIssuerCertificate(endEntityCertificate, trustStore);
if (issuerCertificate != null) {
if (isRoot(issuerCertificate)) {
return issuerCertificate;
} else {
return getRootCertificate(issuerCertificate, trustStore);
}
}
return null;
}
private static boolean isRoot(X509Certificate certificate) {
try {
certificate.verify(certificate.getPublicKey());
return certificate.getKeyUsage() != null && certificate.getKeyUsage()[5];
} catch (Exception e) {
return false;
}
}
First, we attempt to locate the issuer of a provided certificate within the trust store:
static X509Certificate findIssuerCertificate(X509Certificate certificate, KeyStore trustStore)
throws KeyStoreException {
Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
Certificate cert = trustStore.getCertificate(alias);
if (cert instanceof X509Certificate) {
X509Certificate x509Cert = (X509Certificate) cert;
if (x509Cert.getSubjectX500Principal().equals(certificate.getIssuerX500Principal())) {
return x509Cert;
}
}
}
return null;
}
Then, if a match is discovered, we proceed to verify whether the certificate is a self-signed CA certificate. In the event of successful verification, we reach the root certificate. If not, we continue our search.
Finally, let’s test our method to check whether it works properly:
@Test
void whenCertificateIsCASigned_thenRootCanBeFoundInTruststore() throws Exception {
X509Certificate endEntityCertificate = (X509Certificate) keyStore.getCertificate("baeldung");
X509Certificate rootCertificate = getRootCertificate(endEntityCertificate, trustStore);
assertNotNull(rootCertificate);
}
If we perform the same test using the self-signed certificate, we won’t get the root since our trust store doesn’t contain it.
5. Checking if the Certificate Is a CA Certificate
In cases when we’re dealing only with root or intermediate certificates, we may need to perform additional checks.
It’s important to note a root certificate is also a self-signed certificate. However, the difference between the root and the user’s self-signed certificate is the former will have the keyCertSign flag enabled (since we can use it to sign other certificates).
We can identify root or intermediate certificates by checking the key usage:
@Test
void whenCertificateIsCA_thenItCanBeUsedToSignOtherCertificates() throws Exception {
X509Certificate certificate = (X509Certificate) keyStore.getCertificate("cloudflare");
assertTrue(certificate.getKeyUsage()[5]);
}
Moreover, one of the checks we can perform is checking basic constraints extension.
The Basic Constraints extension is a field in X.509 certificates that provides information about the certificate’s intended usage and whether it represents a Certificate Authority (CA) or an end entity.
If the basic constraints extension doesn’t exist, the getBasicConstraints() method returns -1:
@Test
void whenCertificateIsCA_thenBasicConstrainsReturnsZeroOrGreaterThanZero() throws Exception {
X509Certificate certificate = (X509Certificate) keyStore.getCertificate("cloudflare");
assertNotEquals(-1, certificate.getBasicConstraints());
}
6. Conclusion
In this article, we learned how to check whether a certificate is self-signed or CA-signed.
To sum up, self-signed certificates have the same subject and issuer components, and additionally, they can be verified using their own public key.
On the other hand, CA-signed certificates are usually part of the certificate chain. To validate them, we need to create a trust store that contains the trusted root and intermediate certificates and check if the root of the end entity certificate matches one of the trusted certificates.
Finally, if we’re working with root or intermediate certificates, we can identify them by checking whether they’re used for signing other certificates or not.
As always, the entire code examples can be found over on GitHub.