Search code examples
javaauthenticationtomcatclient-certificatescac

What is the standard/modern way to use CAC/PIV card authentication in Java/Tomcat web applications?


(See important edit notes near the end.)

We have been researching about CAC/PIV cards for our web application for a few days now, and have found some great information, but we are still lacking some for our specific needs. We are using Spring Boot, Java, and a Tomcat 9/10 server. Essentially, our question is: what is the industry standard for implementing CAC/PIV card authentication in a Spring Boot, Java web app on a Tomcat server? (Most sources we found are more than 10 years old, and the rest of them are 5-9 years old, so we just wanted some current information on this topic.)

Our web app will have 2 methods of signing in:

  1. Username, password, and TOTP key (we have this working)
  2. Username, password, and CAC/PIV card

(The method required for each user will, in part, be determined based on their role.)

We have had several ideas:

Idea #1: Server Configuration

We have found a few others who suggest using the clientAuth="true" setting on our Tomcat server, or similar. [1] [2] [3] This would be ideal since it seems to be the simplest way to implement this, but since we need to have the 2 methods mentioned above, we don't want the application to always require the badge.

A note here- there appears to be an option to say clientAuth="want" which will request the user's certificate, but will not turn down a user that doesn't present a certificate. I still need to learn how it works. (It may not be recommended to use "want" as it may give warnings in some cases.)

Another note- using clientAuth="true" or "want" may prove successful for some, but it doesn't actually provide us with the method we need. Essentially, it will request a certificate from the instant the server boots up. If set to "true", the server will not allow access to the application unless it accepts/trusts the certificate provided. If set to "want", the server will look for a certificate, but if none are found (or none are trusted) it won't hold on to them but will simply let the user to the application as normal. However, there is no way to request a certificate after that (using any server settings).

To get around this, we thought that we might be able to make some kind of "gateway" app whose sole purpose is to perform the badge authentication. (Maybe something like this max.gov website [4] or the other department sites linked on that page. This implementation on max.gov is exactly what we need: the option for badge reading or some other method.)

We were able to get a server certificate to use in testing this idea! We got everything set up and I figured out the server.xml configuration:

<!-- default connector to redirect to https port 8443 -->
<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1"
    connectionTimeout="20000" redirectPort="8443" maxParameterCount="1000" />

<!-- connector structure for https -->
<Connector
    protocol="org.apache.coyote.http11.Http11NioProtocol"
    port="8444" maxThreads="150" SSLEnabled="true"
    maxParameterCount="1000" secure="true" scheme="https" >
    
    <SSLHostConfig
        sslProtocol="TLSv1.2"
        certificateVerification="true" // true requires a cert, or else it fails
        truststoreFile="C:/Program Files/Java/jdk-17/lib/security/cacerts"
        truststorePassword="changeit">
        
        <Certificate
            certificateKeystoreFile="path/to/keystore.jks"
            certificateKeystorePassword="password" type="RSA" />
    </SSLHostConfig>
</Connector>

I am still learning how to make this work with our application requirements. (See my related question [33] for how we got part of this process to work. Link [3] was great at explaining the process/idea.)

Idea #2: Open-Source Libraries

Use an open-source library from GitHub. We have found a few sources that recommend these:

  • OpenSC [5]: This government site [6] also refers to it, but it seems to be some kind of web extension but it is written mostly in C.
  • web-smart-card [7]: we're not sure how to use it.
  • PKCS#11: this is referenced by a few others [8] [9] [10] but it seems that it's for desktop applications--is that true? And some said it can be slow? And the answer on link #9 uses servlets, which aren't used in Spring Boot applications (as far as I can tell).
  • javax.smartcardio: this library, Smart Card IO [20], has some useful functions, but there are two downsides: 1) there is no requirement for all smart cards to follow the same APDU commands/formats, and 2) it's having challenges working properly when the web app is deployed on the server, i.e., it can't find any card or reader.

We read about some using java applets but they were old discussions. Also, you may have to work with signing the applet, and applets are considered somewhat unsafe or insecure.

Idea #3: Requesting Certificate

Investigating more into the Login.gov implementation of CAC/PIV card authentication, I realized the answer given on link [11] is really how card authentication is performed: "You will have a much easier time figuring this out and finding information if you forget that the users are using a smartcard or a CAC, and just start with the idea that you're going to use client certificates for authentication."

This discussion [22] may provide some additional insights.

Essentially, what I have learned is that you need to have an https url for your web app so that your server can make an SSL/TLS connection--that is the only way that you can request a user's certificate. If you get that, theoretically, you will just have to request the user's X509 certificate with something like this:

X509Certificate[] x509certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");

if (x509certs != null && x509certs.length > 0) {
    X509Certificate x509cert = x509certs[0];
    try {
        x509cert.checkValidity();
    } catch (Exception e) {
        System.out.println("error checking cert or cert is not valid.");
    }
} else {
    System.out.println("certs list is null or empty.");
}

(Make sure that you change javax to jakarta if you are using tomcat 10 or later.)

Additionally, for those who are creating an application for a government entity, you could contact the Login.gov team on their site [17]. If either you or your agency have a .gov email address, you will be able to create a developer's account, and use their system to perform the user authentication.

Idea #4: Direct Card Communication

CAC, PIV, and other cards are all considered smart cards. Computers communicate with smart cards via APDUs (Application Protocol Data Units) [18]. There are some open-source libraries that might work, and Oracle also has some things: Java Card [19], Smart Card IO [20]. The Java Card API is mainly for creating and modifying cards, and generally uses applets, which is not really a good choice for us.

Using Smart Card IO, I can at least check if a card is present and read the card's ATR:

try {
    // get all terminals
    TerminalFactory factory = TerminalFactory.getDefault();
    List<CardTerminal> terminals = factory.terminals().list();
    System.out.println("Terminals: " + terminals);
            
    // get the first terminal
    CardTerminal cdTerminal = terminals.get(0);
    
    try {
        if (cdTerminal.isCardPresent()) {
            // establish a connection with the card
            Card card = cdTerminal.connect("*"); // don't limit the type, just in case your card is different
            System.out.println("card: " + card);
            
            ATR atr = card.getATR();
            System.out.println("ATR: " + atr.getBytes().toString());
            
            card.disconnect(false);
        } else {
            System.out.println("No card inserted!");
        }
    
    } catch (CardException e) {
        System.out.println("Failed to determine if card is present.");
    }
} catch (CardException e1) {
    System.out.println("Failed in smart card io.");
}

I hoped to use the transmitControlCommand() [21] method to get the certificate, but it is challenging to find information on what the two parameters mean, and what options are available for them. (See [24], [25], [26)].

I'm looking into it, but someone has a GitHub repository [27] that uses this library, and I'm trying to learn how they use it.

This library has some useful functions, but, unfortunately, there are two downsides:

  1. There is no requirement for all smart cards to follow the same APDU commands/formats
  2. The library functions have challenges working properly when the web app is deployed on the server, i.e., it can't find any card or reader.

Idea #5: Sun PKCS#11

This discussion [23], and others that I've seen, recommend the Sun PKCS#11 API, but apparently it is deprecated. There are possible alternatives (Bouncy Castle and IAIK PKCS#11 [28]), but they all require some extra, native PKCS#11 library.

Other Ideas:

I like the discussion here [11], but it's a bit more theoretical without any code example. Regardless, if anyone else is looking at a way to do this badge authentication, the answer there has the best foundation for understanding how badge authentication really works!

These 2 bring up some good questions [12] [13], but there are no answers to them.

Some use node.js or React, like [this 14], but I've heard that it's not the greatest to use that in a Spring Boot application. Now I'm looking a little more into it to see what I might be able to use (especially for checking if a card is present).

There is also a Developer Guide [15] for the card reader that is used by the end-users of this app, but I have yet to find anything useful in there.

We have also thought about using the available library from Okta, since they now have the option for PIV card authentication methods [16]. However, it appears that Okta is a paid service, which will not be an option for us.

I found a recommendation to try a lower-level API like PC/SC [29], but that also appears to require APDU commands (which can be card-dependent).

Edits

5-14-2024: We are no longer going to be using the Spring Boot Framework due to the time that would be needed to convert old JDBC code to the new way, using the jdbcTemplate bean. We are also looking at both Tomcat 9 and 10 (for two different applications, both with the same login requirements).

5-30-2024: I have done more research and found plenty of good ideas, but nothing has been completely successful so far. I've added notes above.

6-13-2024: (Misc. edits above.) I have found a simple way to read the certificates from the smart card! Unfortunately, some parts don't work for me in the same way as the sources described: I don't get the popup certificate selection box. The SO questions that inspired me were this one [30] and this one [31], so make sure to check them out if you found my question useful at all!

What I did was this:

// there are various types and providers, but these were the ones that worked
KeyStore keyStore = KeyStore.getInstance("Windows-MY", "SunMSCAPI");
// load local certificates (the certificates visible in your browser)
keyStore.load(null);
// get all the names of the available certificates
Enumeration<String> aliases = keyStore.aliases();

Now I can loop through the Enumeration to see the certificates available! You can then access each one like

String als;
Certificate cert;
while (aliases.hasMoreElements()) {
    als = aliases.nextElement();
    System.out.println("Alias: " + als);
    cert = keyStore.getCertificate(als); // get the certificate
    // check the certificate
    certs = keyStore.getCertificateChain(als); // get the certificate chain
    // check the certificate chain
}

There are some methods of the Certificate class that you can use to check it; for example, cert.getPublicKey(), cert.getType(), and cert.verify() (make sure you use the issuer's public key to verify [32]). If cert instanceof X509Certificate, there are many more methods available!

Even with that progress, I still haven't found a way to verify the card's pin; this only gives me the public information from the certificate on the card.

6-24-2024: I added some notes to Idea #1 which appears to be the best option!

Link references:

  1. Common Access Card (CAC) Authentication Using Java
  2. Trying to use a smartcard to authenticate to Tomcat
  3. https://blog.e-zest.com/enable-tomcat-server-for-smart-card-authentication
  4. https://login.max.gov/cas/login?bypassMaxsso=true
  5. https://github.com/OpenSC/OpenSC
  6. https://www.idmanagement.gov/implement/scl-firefox/
  7. https://github.com/WICG/web-smart-card
  8. https://stackoverflow.com/a/3826528/15811117
  9. Common Access Card (CAC) Authentication using Vaadin Application
  10. DoD PKI CAC authentication in Tomcat (embedded in JBoss)
  11. CAC authentication in a Java WebApp
  12. CAC authentication on tomcat server
  13. Authenticate a user using CAC (Common Access Card) in a web application running in Jetty for an application used by a US government agency
  14. https://marcioreis.pt/how-to-communicate-with-a-smartcard-reader-using-node-js/
  15. https://www.hidglobal.com/documents/omnikey-5422-and-5122-software-developer-guide
  16. https://www.okta.com/blog/2018/05/using-personal-identity-verification-piv-credentials-to-enable-passwordless-authentication/
  17. https://developers.login.gov/
  18. https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit
  19. https://docs.oracle.com/en/java/javacard/3.1/jc_api_srvc/api_classic/index.html
  20. https://docs.oracle.com/javase/8/docs/jre/api/security/smartcardio/spec/javax/smartcardio/package-summary.html
  21. https://docs.oracle.com/javase/8/docs/jre/api/security/smartcardio/spec/javax/smartcardio/Card.html#transmitControlCommand-int-byte:A-
  22. How to get server certificate chain then verify it's valid and trusted in Java
  23. API to read smart card certificate in Java
  24. https://forums.oracle.com/ords/apexds/post/what-is-the-controlcommand-4285
  25. https://forums.oracle.com/ords/apexds/post/use-secure-pin-commands-with-javax-smartcardio-1045
  26. https://stackoverflow.com/a/40673167/15811117
  27. https://github.com/jnasmartcardio/jnasmartcardio/blob/master/src/main/java/jnasmartcardio/Smartcardio.java
  28. How to initialize the PKCS11 provider without using SunPKCS11?
  29. https://pcscworkgroup.com/
  30. Validate when keystore.load() is canceled
  31. Load Java KeyStore for one alias?
  32. Public key verification always returns "Signature does not match"
  33. How to get the popup menu to select user certificate in Tomcat 10 server?

(I'll continue updating this question as I continue my research.)


Solution

  • In short, ideas #1 and #3 are the solution to the problem.

    My question and answer here also show some important parts of the solution. Again, I'll point out that link 11 in my question above is very important.

    Configure the server.xml file as described in my answer here and use something like the code from idea #3 above:

    X509Certificate[] x509certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
    
    if (x509certs != null && x509certs.length > 0) {
        X509Certificate x509cert = x509certs[0];
        try {
            x509cert.checkValidity();
        } catch (Exception e) {
            System.out.println("error checking cert or cert is not valid.");
        }
    } else {
        System.out.println("certs list is null or empty.");
    }
    

    Note: the browser prompts the user to select their certificate and enter their, not the web application. Then, the server obtains the certificate (chain) from the browser.