001package com.hfg.cert;
002
003import java.net.URL;
004import java.security.KeyStore;
005import java.security.PublicKey;
006import java.security.cert.*;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collection;
010import java.util.Collections;
011import java.util.HashSet;
012import java.util.List;
013import java.util.Set;
014import java.util.regex.Matcher;
015import java.util.regex.Pattern;
016import java.util.stream.Collectors;
017import javax.net.ssl.HttpsURLConnection;
018import javax.net.ssl.SSLContext;
019import javax.net.ssl.SSLSocket;
020import javax.net.ssl.SSLSocketFactory;
021import javax.net.ssl.TrustManager;
022import javax.net.ssl.TrustManagerFactory;
023import javax.net.ssl.X509TrustManager;
024
025import com.hfg.util.StringBuilderPlus;
026import com.hfg.util.StringUtil;
027import com.hfg.util.collection.OrderedSet;
028
029//------------------------------------------------------------------------------
030/**
031 * General certificate utility methods.
032 *
033 * @author J. Alex Taylor, hairyfatguy.com
034 */
035//------------------------------------------------------------------------------
036// com.hfg XML/HTML Coding Library
037//
038// This library is free software; you can redistribute it and/or
039// modify it under the terms of the GNU Lesser General Public
040// License as published by the Free Software Foundation; either
041// version 2.1 of the License, or (at your option) any later version.
042//
043// This library is distributed in the hope that it will be useful,
044// but WITHOUT ANY WARRANTY; without even the implied warranty of
045// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
046// Lesser General Public License for more details.
047//
048// You should have received a copy of the GNU Lesser General Public
049// License along with this library; if not, write to the Free Software
050// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
051//
052// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
053// jataylor@hairyfatguy.com
054//------------------------------------------------------------------------------
055
056public class CertificateUtil
057{
058
059   //--------------------------------------------------------------------------
060   public static void verifyCertificates(String inURL)
061         throws Exception
062   {
063      Set<X509Certificate> certs;
064
065      if (inURL.startsWith("https:"))
066      {
067         certs = getWebCertificates(inURL);
068      }
069      else if (inURL.startsWith("ldaps:"))
070      {
071         certs = geLdapCertificates(inURL);
072      }
073      else
074      {
075         throw new RuntimeException("Unsupported url protocol: " + StringUtil.singleQuote(inURL) + "!");
076      }
077
078
079      List<X509Certificate> trustedCerts = getTrustedCertificates();
080      Set<X509Certificate> rootCAs = new HashSet<>(trustedCerts.size());
081      Set<X509Certificate> intermediateCerts = new HashSet<>(trustedCerts.size());
082      for (X509Certificate cert : trustedCerts)
083      {
084         if (isSelfSigned(cert))
085         {
086            rootCAs.add(cert);
087         }
088         else
089         {
090            intermediateCerts.add(cert);
091         }
092      }
093
094      List<X509Certificate> reorderedCerts = new ArrayList<>(certs);
095      if (reorderedCerts.size() > 1
096          && reorderedCerts.get(0).getIssuerDN().equals(reorderedCerts.get(1).getSubjectDN()))
097      {
098         Collections.reverse(reorderedCerts);
099      }
100
101      for (X509Certificate cert : reorderedCerts)
102      {
103         System.out.println(generateCertSummary(cert));
104
105         X509CertSelector selector = new X509CertSelector();
106         selector.setCertificate(cert);
107
108         Set<TrustAnchor> trustAnchors = new HashSet<>();
109         for (X509Certificate rootCA : rootCAs)
110         {
111            trustAnchors.add(new TrustAnchor(rootCA, null));
112         }
113
114         PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(trustAnchors, selector);
115
116         pkixParams.setRevocationEnabled(false);
117
118         CertStore intermediateCertStore = CertStore.getInstance("Collection",
119                                                                 new CollectionCertStoreParameters(intermediateCerts));
120         pkixParams.addCertStore(intermediateCertStore);
121
122         CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
123         PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder.build(pkixParams);
124
125         if (isSelfSigned(cert))
126         {
127            rootCAs.add(cert);
128         }
129         else
130         {
131            intermediateCerts.add(cert);
132         }
133      }
134   }
135
136   //--------------------------------------------------------------------------
137   public static String generateCertSummary(X509Certificate inCert)
138      throws CertificateParsingException
139   {
140      List<SAN> sanNames = getSubjectAlternateNames(inCert);
141
142      StringBuilderPlus buffer = new StringBuilderPlus()
143            .appendln("Certificate Info:")
144            .appendln("----------------")
145            .appendln("   Subject DN: " + inCert.getSubjectDN())
146            .appendln("         Type: " + inCert.getType())
147            .appendln("   Public Key Algorithm: " + inCert.getPublicKey().getAlgorithm())
148            .appendln("   Public Key Format: " + inCert.getPublicKey().getFormat())
149            .appendln(" Subject Alt Name(s): " + (sanNames != null ? StringUtil.join(sanNames, ", ") : ""))
150            .appendln("    Issuer DN: " + inCert.getIssuerDN())
151            .appendln("      Expires: " + inCert.getNotAfter());
152
153      return buffer.toString();
154   }
155
156   //--------------------------------------------------------------------------
157   public static Set<X509Certificate> getWebCertificates(String inURL)
158         throws Exception
159   {
160      HttpsURLConnection conn = (HttpsURLConnection) new URL(inURL).openConnection();
161      conn.connect();
162
163      Set<X509Certificate> certSet = new OrderedSet<>(5);
164
165      Certificate[] certs = conn.getServerCertificates();
166      if (certs != null)
167      {
168         certSet.addAll((List<X509Certificate>) (Object) Arrays.asList(certs));
169      }
170
171      return certSet;
172   }
173
174   //--------------------------------------------------------------------------
175   public static Set<X509Certificate> geLdapCertificates(String inURL)
176         throws Exception
177   {
178      Pattern urlPattern = Pattern.compile("ldap(?:s)?://([\\w\\-\\.]+)(?:\\:(\\d+))?");
179      Matcher m = urlPattern.matcher(inURL);
180      if (! m.matches())
181      {
182         throw new RuntimeException("Unrecognized URL pattern: " + inURL);
183      }
184
185      String host = m.group(1);
186      int port = (m.group(2) != null ? Integer.parseInt(m.group(2)) : 636);
187
188      TrustManagerFactory trustMgrfactory = getTrustManagerFactory();
189
190      X509TrustManager defaultTrustManager = (X509TrustManager) trustMgrfactory.getTrustManagers()[0];
191
192      SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
193
194      SSLSocket sslSocket = null;
195
196      SSLContext context = SSLContext.getInstance("TLS");
197      context.init(null, new TrustManager[]{tm}, null);
198      SSLSocketFactory sslFactory = context.getSocketFactory();
199
200      try
201      {
202         sslSocket = (SSLSocket) sslFactory.createSocket(host, port);
203         sslSocket.setSoTimeout(5000);
204         System.out.println("Starting SSL handshake...");
205         sslSocket.startHandshake();
206      }
207      catch (Exception e)
208      {
209         // Ignore. We want to be able to examine the certificates even if there was
210         // an error thrown because they aren't recognized or valid.
211      }
212      finally
213      {
214         if (sslSocket != null)
215         {
216            sslSocket.close();
217         }
218      }
219
220      return tm.mNewCertificates;
221   }
222
223   //--------------------------------------------------------------------------
224   private static List<X509Certificate> getTrustedCertificates()
225      throws Exception
226   {
227      TrustManagerFactory factory = getTrustManagerFactory();
228
229      List<TrustManager> trustManagers = Arrays.asList(factory.getTrustManagers());
230      List<X509Certificate> certificates = trustManagers.stream()
231            .filter(X509TrustManager.class::isInstance)
232            .map(X509TrustManager.class::cast)
233            .map(trustManager -> Arrays.asList(trustManager.getAcceptedIssuers()))
234            .flatMap(Collection::stream)
235            .collect(Collectors.toList());
236
237      return certificates;
238   }
239
240   //--------------------------------------------------------------------------
241   private static TrustManagerFactory getTrustManagerFactory()
242      throws Exception
243   {
244      TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
245      factory.init((KeyStore) null);
246
247      return factory;
248   }
249
250   //--------------------------------------------------------------------------
251   public static boolean isSelfSigned(X509Certificate inCert)
252   {
253      boolean result = true;
254
255      try
256      {
257         PublicKey key = inCert.getPublicKey();
258         inCert.verify(key);
259      }
260      catch (Exception e)
261      {
262         result = false;
263      }
264
265      return result;
266   }
267
268   //--------------------------------------------------------------------------
269   public static List<SAN> getSubjectAlternateNames(X509Certificate inCertificate)
270      throws CertificateParsingException
271   {
272      List<SAN> values = null;
273      try
274      {
275         /*
276          The ASN.1 definition of the SubjectAltName extension is:
277          SubjectAltName ::= GeneralNames
278
279          GeneralNames :: = SEQUENCE SIZE (1..MAX) OF GeneralName
280
281          GeneralName ::= CHOICE {
282               otherName                       [0]     OtherName,
283               rfc822Name                      [1]     IA5String,
284               dNSName                         [2]     IA5String,
285               x400Address                     [3]     ORAddress,
286               directoryName                   [4]     Name,
287               ediPartyName                    [5]     EDIPartyName,
288               uniformResourceIdentifier       [6]     IA5String,
289               iPAddress                       [7]     OCTET STRING,
290               registeredID                    [8]     OBJECT IDENTIFIER}
291
292          If this certificate does not contain a SubjectAltName extension, null is returned.
293          Otherwise, a Collection is returned with an entry representing each GeneralName
294          included in the extension. Each entry is a List whose first entry is an Integer
295          (the name type, 0-8) and whose second entry is a String or a byte array (the name,
296          in string or ASN.1 DER encoded form, respectively).
297          */
298         Collection<List<?>> altNames = inCertificate.getSubjectAlternativeNames();
299         if (altNames != null)
300         {
301            values = new ArrayList<>(3);
302            for (List item : altNames)
303            {
304               SAN_Type type = SAN_Type.valueOf((Integer) item.get(0));
305               Object value = item.get(1);
306               String stringValue = null;
307
308               if (SAN_Type.OTHER_NAME.equals(type))
309               {
310                  if (value instanceof String)
311                  {
312                     stringValue = (String) value;
313                  }
314                  else if (value instanceof byte[])
315                  {
316                     // ASN.1 DER-encoded value
317                     // TODO: Decode the value
318                     stringValue = "DER-encoded value";
319                        /*
320                        try
321                        {
322                           ASN1InputStream decoder=null;
323                           if(item.toArray()[1] instanceof byte[])
324                              decoder = new ASN1InputStream((byte[]) item.toArray()[1]);
325                           else if(item.toArray()[1] instanceof String)
326                              identities.add( (String) item.toArray()[1] );
327                           if(decoder==null) continue;
328                           DEREncodable encoded = decoder.readObject();
329                           encoded = ((DERSequence) encoded).getObjectAt(1);
330                           encoded = ((DERTaggedObject) encoded).getObject();
331                           encoded = ((DERTaggedObject) encoded).getObject();
332                           String identity = ((DERUTF8String) encoded).getString();
333                           identities.add(identity);
334                        }
335                        catch (UnsupportedEncodingException e) {
336                           log.error("Error decoding subjectAltName" + e.getLocalizedMessage(),e);
337                        }
338                        catch (Exception e) {
339                           log.error("Error decoding subjectAltName" + e.getLocalizedMessage(),e);
340                        }
341                        */
342                  }
343               }
344               else if (SAN_Type.RFC822_NAME.equals(type)
345                        || SAN_Type.DNS_NAME.equals(type))
346               {
347                  if (value instanceof String)
348                  {
349                     stringValue = (String) value;
350                  }
351               }
352               else if (SAN_Type.X400_ADDRESS.equals(type))
353               {
354                  // TODO:
355                  if (value instanceof String)
356                  {
357                     stringValue = (String) value;
358                  }
359               }
360               else if (SAN_Type.DIRECTORY_NAME.equals(type))
361               {
362                  // TODO:
363                  if (value instanceof String)
364                  {
365                     stringValue = (String) value;
366                  }
367               }
368               else if (SAN_Type.EDI_PARTY_NAME.equals(type))
369               {
370                  // TODO:
371                  if (value instanceof String)
372                  {
373                     stringValue = (String) value;
374                  }
375               }
376               else if (SAN_Type.URI.equals(type))
377               {
378                  // TODO:
379                  if (value instanceof String)
380                  {
381                     stringValue = (String) value;
382                  }
383               }
384               else if (SAN_Type.IP_ADDRESS.equals(type))
385               {
386                  // TODO:
387                  if (value instanceof String)
388                  {
389                     stringValue = (String) value;
390                  }
391               }
392               else if (SAN_Type.REGISTERED_ID.equals(type))
393               {
394                  // TODO:
395                  if (value instanceof String)
396                  {
397                     stringValue = (String) value;
398                  }
399               }
400
401               values.add(new SAN(type, stringValue));
402            }
403         }
404      }
405      catch (CertificateParsingException e)
406      {
407         throw new CertificateParsingException("Error parsing SubjectAltName in certificate: " + inCertificate,e);
408      }
409
410      return values;
411   }
412
413   private static class SavingTrustManager implements X509TrustManager
414   {
415      private X509TrustManager mParentTrustMgr;
416      private Set<X509Certificate> mAllCertificates = new HashSet<>(3);
417      private Set<X509Certificate> mNewCertificates = new HashSet<>(3);
418
419
420      //-----------------------------------------------------------------------
421      SavingTrustManager(X509TrustManager inParentTrustMgr)
422      {
423         mParentTrustMgr = inParentTrustMgr;
424      }
425
426      //-----------------------------------------------------------------------
427      @Override
428      public void checkClientTrusted(X509Certificate[] inCertChain, String inAuthType)
429            throws CertificateException
430      {
431         mParentTrustMgr.checkClientTrusted(inCertChain, inAuthType);
432      }
433
434      //-----------------------------------------------------------------------
435      @Override
436      public void checkServerTrusted(X509Certificate[] inCertChain, String inAuthType)
437         throws CertificateException
438      {
439         CertificateException exceptionToRethrow = null;
440         // check the certificate chain against the system truststore
441         try
442         {
443            mParentTrustMgr.checkServerTrusted(inCertChain, inAuthType);
444         }
445         catch (CertificateException e)
446         {
447            // The certificate chain was found untrustworthy
448
449            // Check if the first certificate in the chain is not known yet stored
450            if (! this.mAllCertificates.contains(inCertChain[0]))
451            {
452               // Save the exception to be re-thrown later if not known
453               exceptionToRethrow = e;
454            }
455         }
456
457
458         // Save the full chain to both local accumulators
459         for (X509Certificate cert : inCertChain)
460         {
461            mAllCertificates.add(cert);
462            mNewCertificates.add(cert);
463         }
464
465         // check and re-throw the exception if any
466         if (exceptionToRethrow != null)
467         {
468            throw exceptionToRethrow;
469         }
470      }
471
472      //-----------------------------------------------------------------------
473      @Override
474      public X509Certificate[] getAcceptedIssuers()
475      {
476         return mParentTrustMgr.getAcceptedIssuers();
477      }
478   }
479}