5

Consider the following sample code which uses a TrustManager to log whether an outgoing connection used a valid certificate (but accept the connection in all cases):

import java.security.*;
import java.security.cert.*;
import javax.net.ssl.*;

public class CertChecker implements X509TrustManager {

    private final X509TrustManager defaultTM;

    public CertChecker() throws GeneralSecurityException {
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init((KeyStore)null);
        defaultTM = (X509TrustManager) tmf.getTrustManagers()[0];
    }

    public void checkServerTrusted(X509Certificate[] certs, String authType) {
        if (defaultTM != null) {
            try {
                defaultTM.checkServerTrusted(certs, authType);
                System.out.println("Certificate valid");
            } catch (CertificateException ex) {
                System.out.println("Certificate invalid: " + ex.getMessage());
            }
        }
    }

    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
    public X509Certificate[] getAcceptedIssuers() { return null;}

    public static void main(String[] args) throws Exception {
        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, new TrustManager[] {new CertChecker()}, new SecureRandom());
        SSLSocketFactory ssf = (SSLSocketFactory) sc.getSocketFactory();
        ((SSLSocket)ssf.createSocket(args[0], 443)).startHandshake();
    }
}

What do I have to do inside the checkClientTrusted method to check if that certificate is an extended validation certificate (green address bar in modern browsers) or a normal one (yellow address bar)?

edit:

I'm trying to get a CertPathValidator working, but somehow I only get exceptions about certificate is not a CA certificate... Any ideas?

edit2: Using PKIXParameters instead of PKIXBuilderParameters

private boolean isEVCertificate(X509Certificate[] certs, String authType) {
    try {
        CertPath cp = new X509CertPath(Arrays.asList(certs));
        KeyStore ks = KeyStore.getInstance("JKS");
        ks.load(new FileInputStream(new File(System.getProperty("java.home"), "lib/security/cacerts")), null);
        PKIXParameters cpp = new PKIXParameters(ks);
        cpp.setRevocationEnabled(false);
        CertPathValidator cpv = CertPathValidator.getInstance("PKIX");          
        PKIXCertPathValidatorResult res = (PKIXCertPathValidatorResult) cpv.validate(cp, cpp);
        System.out.println(res.getTrustAnchor().getCAName());
        System.out.println(res.getPolicyTree().getValidPolicy());
        System.out.println(cp);
        return false;
    } catch (Exception ex) {
        ex.printStackTrace();
        return false;
    }
}

I am testing against real-world EV certificates. The code now works with www.paypal.com (in the sense that it does not throw an exception), but does not work with banking.dkb.de. :-(

But even with Paypal.com the trust anchor getCAName returns null, so how can I know against which CA it was validated so that I can look up the right EV policy?

4
  • Your validator code is a little different in that you are using PKIXBuilderParameters for validation; normally I'd use just PKIXParameters (the target is already known). Also, is the certificate chain you are using issued through a "real" CA, or a test one that you signed yourself? If the latter, I'd be concerned about various extensions. They can be very tricky to get right so that they satisfy the PKIX validator.
    – erickson
    Commented Nov 11, 2009 at 19:37
  • updated my question accordingly...
    – mihi
    Commented Nov 12, 2009 at 15:46
  • Okay, check out my example. It works for https://banking.dkb.de/dkb/.
    – erickson
    Commented Nov 12, 2009 at 21:02
  • Okay, I see the problem with https://banking.dkb.de/. It's actually a misconfiguration of their server: they are sending one of the intermediate certificates twice, which makes an invalid cert chain if you use it directly. When I was using my isEV method, I actually used a CertPathBuilder, and passed it the whole chain that I received from the server. The builder was able to make a correct chain, ignoring the unnecessary certs the server is mistakenly sending. If this is correct, the EV test itself shouldn't be failing, it should be the path validation.
    – erickson
    Commented Nov 14, 2009 at 23:19

3 Answers 3

4

First, you'll need a table of issuer names and their corresponding EV policy identifiers.

When a CA issues a certificate, they can note the policy under which they issued the certificate. The identifier for this policy assigned by the issuer, so that's why you need a list of issuers and their EV policies.

Then you'll need to get the policy from the server certificate. Refer to RFC 5280, § 4.1.2.4 to learn more about policies in general and how they work.

You'll need to validate the certificate chain to obtain a PKIXCertPathValidatorResult. Part of the result is a policy tree. You can navigate through the policy tree to determine if it includes the EV policy for the target certificate's issuer.


Here's a detailed example of checking a certificate path result.

private static final Map<X500Principal, String> policies = new HashMap<X500Principal, String>();

static {
  /* 
   * It would make sense to populate this map from Properties loaded through 
   * Class.getResourceAsStream().
   */
  policies.put(
    new X500Principal("OU=Class 3 Public Primary Certification Authority,O=VeriSign\\, Inc.,C=US"), 
    "2.16.840.1.113733.1.7.23.6"
  );
  // ...
}

static boolean isEV(PKIXCertPathValidatorResult result)
{
  /* Determine the policy to look for. */
  X500Principal root = result.getTrustAnchor().getTrustedCert().getSubjectX500Principal();
  String policy = policies.get(root);
  if (policy == null)
    /* The EV policy for this issuer is unknown (or there is none). */
    return false;
  /* Traverse the tree, looking at its "leaves" to see if the end-entity 
   * certificate was issued under the corresponding EV policy. */
  PolicyNode tree = result.getPolicyTree();
  Deque<PolicyNode> stack = new ArrayDeque<PolicyNode>();
  stack.push(tree);
  while (!stack.isEmpty()) {
    PolicyNode current = stack.pop();
    Iterator<? extends PolicyNode> children = current.getChildren();
    int leaf = stack.size();
    while (children.hasNext())
      stack.push(children.next());
    if (stack.size() == leaf) {
      /* If the stack didn't grow, there were no "children". I.e., the 
       * current node is a "leaf" node of the policy tree. */
      if (current.getValidPolicy().equals(policy))
        return true;
    }
  }
  /* The certificate wasn't issued under the authority's EV policy. */
  return false;
}
6
  • There's no library for this!?
    – jrockway
    Commented Nov 7, 2009 at 22:59
  • Not that I know of. It's not a very common requirement; EV certificates are most useful in browsers, and there aren't many of those.
    – erickson
    Commented Nov 8, 2009 at 0:38
  • I don't get that CertPathValidator to work. My code is above. Please help me how to initialize it so that it works and does not throw exceptions...
    – mihi
    Commented Nov 9, 2009 at 13:24
  • Although I did not get it to work; I'm just accepting your answer since it was the most useful one :)
    – mihi
    Commented Nov 11, 2009 at 18:34
  • Your example works fine for me for www.paypal.com, but not for banking.dkb.de :( I guess something is wrong in my JDK/security settings/cacert file... I just give up now, thank you very much for your help :-)
    – mihi
    Commented Nov 14, 2009 at 18:56
3

EDIT: Posted addtional code.

If you use Sun's X509 implementation, you can do something like this,

  CertificatePoliciesExtension ext = ((X509CertImpl)cert).getCertificatePoliciesExtension();
  List<PolicyInformation> policies = (List<PolicyInformation>)ext.get(CertificatePoliciesExtension.POLICIES);
  boolean evCert = false;
  for (PolicyInformation info : policies) {
      CertificatePolicyId id = info.getPolicyIdentifier();
      if (isEVPolicy(id)) {
         evCert = true;
         break;                 
      }             
  }

  ......

  public static ObjectIdentifier[] EV_POLICIES;

  static {
      try {
          EV_POLICIES = new ObjectIdentifier[] {
                new ObjectIdentifier("2.16.840.1.113733.1.7.23.6"), // Verisign
                new ObjectIdentifier("1.3.6.1.4.1.14370.1.6"), // Geo-Trust of Verisign
                new ObjectIdentifier("2.16.840.1.113733.1.7.48.1") // Thawte
          };
      } catch (IOException e) {
        throw new IllegalStateException("Invalid OIDs");
      }
  }

  private boolean isEVPolicy(CertificatePolicyId id) {
    for (ObjectIdentifier oid : EV_POLICIES) {
        if (oid.equals((Object)id.getIdentifier())) 
            return true;
    }
    return false;
 }

We only allow EV cert from 3 CAs. You can add more EV OIDs in that array. You can get a full list of the OIDs from

http://hg.mozilla.org/mozilla-central/file/05ab1cbc361f/security/manager/ssl/src/nsIdentityChecking.cpp

7
  • No you can't. This will give you all policy extensions and not just the ones indicating that the certificate fulfills the "extended validation" requirements.
    – jarnbjo
    Commented Nov 8, 2009 at 0:46
  • This is just part of the code that checks for EV. However, this is enough if you just want to know if the cert has EV. Cert Policies Extension is added for EV cert and no CA uses for other purposes so far. Show me a real cert with other policies.
    – ZZ Coder
    Commented Nov 8, 2009 at 0:51
  • 2
    My bank is e.g. using a regular class 3 Verisign certificate with policy 2.16.840.1.113733.1.7.23.3 (non-EV).
    – jarnbjo
    Commented Nov 8, 2009 at 1:02
  • 1
    Also, your code is helpful, but its still not complete. For example, you need to make sure the policy is valid, given the policies higher up the cert path. Also, at a minimum, make sure that the policy ID matches the issuer.
    – erickson
    Commented Nov 8, 2009 at 4:52
  • 1
    The process to validate EV cert is indeed complex. Unfortunately, I can't post the whole class (800 lines long). We copied our logic from Firefox. Please refer to the C++ code mentioned in my answer for complete logic.
    – ZZ Coder
    Commented Nov 8, 2009 at 6:47
3

I finally got it to work... A running minimal example that shows all the logic and checks is below. And yes, it works for banking.dkb.de :-)

Thanks to all of you who helped me. Any comments about blatant security holes or anything else (except the code style or missing error handling; I tried hard to condense my code to the absolute minimum of runnable code) are welcome, so feel free to comment :)

import java.io.*;
import java.security.*;
import java.security.cert.*;
import java.util.*;

import javax.net.ssl.*;
import javax.security.auth.x500.X500Principal;

public class CertChecker implements X509TrustManager {

    private final X509TrustManager defaultTM;

    public CertChecker() throws GeneralSecurityException {
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init((KeyStore)null);
        defaultTM = (X509TrustManager) tmf.getTrustManagers()[0];
    }

    public void checkServerTrusted(X509Certificate[] certs, String authType) {
        if (defaultTM != null) {
            try {
                defaultTM.checkServerTrusted(certs, authType);
                if (isEVCertificate(certs))
                    System.out.println("EV Certificate: "+ certs[0].getSubjectX500Principal().getName() + " issued by " + certs[0].getIssuerX500Principal().getName());                    
                System.out.println("Certificate valid");
            } catch (CertificateException ex) {
                System.out.println("Certificate invalid: " + ex.getMessage());
            }
        }
    }

    private boolean isEVCertificate(X509Certificate[] certs) {
        try {
            // load keystore with trusted CA certificates
            KeyStore cacerts = KeyStore.getInstance("JKS");
            cacerts.load(new FileInputStream(new File(System.getProperty("java.home"), "lib/security/cacerts")), null);

            // build a cert selector that selects the first certificate of the certificate chain
            // TODO we should verify this against the hostname...
            X509CertSelector targetConstraints = new X509CertSelector();
            targetConstraints.setSubject(certs[0].getSubjectX500Principal());

            // build a cert path from our selected cert to a CA cert
            PKIXBuilderParameters params = new PKIXBuilderParameters(cacerts, targetConstraints);        
            params.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(Arrays.asList(certs))));
            params.setRevocationEnabled(false);
            CertPath cp = CertPathBuilder.getInstance("PKIX").build(params).getCertPath();

            // validate the cert path
            PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) CertPathValidator.getInstance("PKIX").validate(cp, params);
            return isEV(result);
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }

    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
    public X509Certificate[] getAcceptedIssuers() { return null;}

    public static void main(String[] args) throws Exception {
        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, new TrustManager[] {new CertChecker()}, new SecureRandom());
        SSLSocketFactory ssf = (SSLSocketFactory) sc.getSocketFactory();
        ((SSLSocket)ssf.createSocket(args[0], 443)).startHandshake();
    }

    private static final Map<X500Principal, String> policies = new HashMap<X500Principal, String>();

    static {
        // It would make sense to populate this map from Properties loaded through 
        // Class.getResourceAsStream().
        policies.put(
                new X500Principal("OU=Class 3 Public Primary Certification Authority,O=VeriSign\\, Inc.,C=US"), 
                "2.16.840.1.113733.1.7.23.6"
        );
        // TODO add more certificates here
    }

    // based on http://stackoverflow.com/questions/1694466/1694720#1694720
    static boolean isEV(PKIXCertPathValidatorResult result)
    {
        // Determine the policy to look for.
        X500Principal root = result.getTrustAnchor().getTrustedCert().getSubjectX500Principal();
        System.out.println("[Debug] Found root DN: "+root.getName());
        String policy = policies.get(root);
        if (policy != null)
            System.out.println("[Debug] EV Policy should be: "+policy);

        // Traverse the tree, looking at its "leaves" to see if the end-entity 
        // certificate was issued under the corresponding EV policy.
        PolicyNode tree = result.getPolicyTree();
        if (tree == null)
            return false;
        Deque<PolicyNode> stack = new ArrayDeque<PolicyNode>();
        stack.push(tree);
        while (!stack.isEmpty()) {
            PolicyNode current = stack.pop();
            Iterator<? extends PolicyNode> children = current.getChildren();
            int leaf = stack.size();
            while (children.hasNext())
                stack.push(children.next());
            if (stack.size() == leaf) {
                System.out.println("[Debug] Found policy: " + current.getValidPolicy());
                // If the stack didn't grow, there were no "children". I.e., the 
                // current node is a "leaf" node of the policy tree.
                if (current.getValidPolicy().equals(policy))
                    return true;
            }
        }
        // The certificate wasn't issued under the authority's EV policy.
        return false;
    }
}
1
  • Wow! Thank you! That really helped me!
    – MicD
    Commented Oct 17, 2019 at 9:50

Not the answer you're looking for? Browse other questions tagged or ask your own question.