1 /*
   2  * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.io.ByteArrayInputStream;
  25 import java.io.FileInputStream;
  26 import java.io.IOException;
  27 import java.io.PrintStream;
  28 import java.net.URI;
  29 import java.net.URISyntaxException;
  30 import java.security.InvalidAlgorithmParameterException;
  31 import java.security.KeyStore;
  32 import java.security.KeyStoreException;
  33 import java.security.NoSuchAlgorithmException;
  34 import java.security.Security;
  35 import java.security.cert.CertPath;
  36 import java.security.cert.CertPathValidator;
  37 import java.security.cert.CertPathValidatorException;
  38 import java.security.cert.CertificateException;
  39 import java.security.cert.CertificateExpiredException;
  40 import java.security.cert.CertificateFactory;
  41 import java.security.cert.CertificateRevokedException;
  42 import java.security.cert.PKIXParameters;
  43 import java.security.cert.PKIXRevocationChecker;
  44 import java.security.cert.X509Certificate;
  45 import java.text.DateFormat;
  46 import java.text.ParseException;
  47 import java.text.SimpleDateFormat;
  48 import java.util.ArrayList;
  49 import java.util.Date;
  50 import java.util.EnumSet;
  51 import java.util.Locale;
  52 
  53 /**
  54  * Utility class to validate certificate path. It supports OCSP and/or CRL
  55  * validation.
  56  */
  57 public class ValidatePathWithParams {
  58 
  59     private static final String FS = System.getProperty("file.separator");
  60     private static final String CACERTS_STORE = System.getProperty("test.jdk")
  61             + FS + "lib" + FS + "security" + FS + "cacerts";
  62 
  63     private final String[] trustedRootCerts;
  64 
  65     // use this for expired cert validation
  66     private Date validationDate = null;
  67 
  68     // expected certificate status
  69     private Status expectedStatus = Status.UNKNOWN;
  70     private Date expectedRevDate = null;
  71 
  72     private final CertPathValidator certPathValidator;
  73     private final PKIXRevocationChecker certPathChecker;
  74     private final CertificateFactory cf;
  75 
  76     /**
  77      * Possible status values supported for EE certificate
  78      */
  79     public static enum Status {
  80         UNKNOWN, GOOD, REVOKED, EXPIRED;
  81     }
  82 
  83     /**
  84      * Constructor
  85      *
  86      * @param additionalTrustRoots trusted root certificates
  87      * @throws IOException
  88      * @throws CertificateException
  89      * @throws NoSuchAlgorithmException
  90      */
  91     public ValidatePathWithParams(String[] additionalTrustRoots)
  92             throws IOException, CertificateException, NoSuchAlgorithmException {
  93 
  94         cf = CertificateFactory.getInstance("X509");
  95         certPathValidator = CertPathValidator.getInstance("PKIX");
  96         certPathChecker
  97                 = (PKIXRevocationChecker) certPathValidator.getRevocationChecker();
  98 
  99         //trustedRootCerts = trustRoots;
 100         if ((additionalTrustRoots == null) || (additionalTrustRoots[0] == null)) {
 101             trustedRootCerts = null;
 102         } else {
 103             trustedRootCerts = additionalTrustRoots.clone();
 104         }
 105     }
 106 
 107     /**
 108      * Validate certificates
 109      *
 110      * @param certsToValidate Certificates to validate
 111      * @param st expected certificate status
 112      * @param revDate if revoked, expected revocation date
 113      * @param out PrintStream to log messages
 114      * @throws IOException
 115      * @throws CertificateException
 116      * @throws InvalidAlgorithmParameterException
 117      * @throws ParseException
 118      * @throws NoSuchAlgorithmException
 119      * @throws KeyStoreException
 120      */
 121     public void validate(String[] certsToValidate,
 122             Status st,
 123             String revDate,
 124             PrintStream out)
 125             throws IOException, CertificateException,
 126             InvalidAlgorithmParameterException, ParseException,
 127             NoSuchAlgorithmException, KeyStoreException {
 128 
 129         expectedStatus = st;
 130         if (expectedStatus == Status.REVOKED) {
 131             if (revDate != null) {
 132                 expectedRevDate = new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy",
 133                         Locale.US).parse(revDate);
 134             }
 135         }
 136 
 137         Status certStatus = null;
 138         Date revocationDate = null;
 139 
 140         logSettings(out);
 141 
 142         try {
 143             doCertPathValidate(certsToValidate, out);
 144             certStatus = Status.GOOD;
 145         } catch (IOException ioe) {
 146             // Some machines don't have network setup correctly to be able to
 147             // reach outside world, skip such failures
 148             out.println("WARNING: Network setup issue, skip this test");
 149             ioe.printStackTrace(System.err);
 150             return;
 151         } catch (CertPathValidatorException cpve) {
 152             out.println("Received exception: " + cpve);
 153 
 154             if (cpve.getCause() instanceof IOException) {
 155                 out.println("WARNING: CertPathValidatorException caused by IO"
 156                         + " error, skip this test");
 157                 return;
 158             }
 159 
 160             if (cpve.getReason() == CertPathValidatorException.BasicReason.ALGORITHM_CONSTRAINED) {
 161                 out.println("WARNING: CertPathValidatorException caused by"
 162                         + " restricted algorithm, skip this test");
 163                 return;
 164             }
 165 
 166             if (cpve.getReason() == CertPathValidatorException.BasicReason.REVOKED
 167                     || cpve.getCause() instanceof CertificateRevokedException) {
 168                 certStatus = Status.REVOKED;
 169                 if (cpve.getCause() instanceof CertificateRevokedException) {
 170                     CertificateRevokedException cre
 171                             = (CertificateRevokedException) cpve.getCause();
 172                     revocationDate = cre.getRevocationDate();
 173                 }
 174             } else if (cpve.getReason() == CertPathValidatorException.BasicReason.EXPIRED
 175                     || cpve.getCause() instanceof CertificateExpiredException) {
 176                 certStatus = Status.EXPIRED;
 177             } else {
 178                 throw new RuntimeException(
 179                         "TEST FAILED: couldn't determine EE certificate status");
 180             }
 181         }
 182 
 183         out.println("Expected Certificate status: " + expectedStatus);
 184         out.println("Certificate status after validation: " + certStatus.name());
 185 
 186         // Don't want test to fail in case certificate is expired when not expected
 187         // Simply skip the test.
 188         if (expectedStatus != Status.EXPIRED && certStatus == Status.EXPIRED) {
 189             out.println("WARNING: Certificate expired, skip the test");
 190             return;
 191         }
 192 
 193         if (certStatus != expectedStatus) {
 194             throw new RuntimeException(
 195                     "TEST FAILED: unexpected status of EE certificate");
 196         }
 197 
 198         if (certStatus == Status.REVOKED) {
 199             // Check revocation date
 200             if (revocationDate != null) {
 201                 out.println(
 202                         "Certificate revocation date:" + revocationDate.toString());
 203                 if (expectedRevDate != null) {
 204                     out.println(
 205                             "Expected revocation date:" + expectedRevDate.toString());
 206                     if (!expectedRevDate.equals(revocationDate)) {
 207                         throw new RuntimeException(
 208                                 "TEST FAILED: unexpected revocation date");
 209                     }
 210                 }
 211             } else {
 212                 throw new RuntimeException("TEST FAILED: no revocation date");
 213             }
 214         }
 215     }
 216 
 217     private void logSettings(PrintStream out) {
 218         out.println();
 219         out.println("=====================================================");
 220         out.println("CONFIGURATION");
 221         out.println("=====================================================");
 222         out.println("http.proxyHost :" + System.getProperty("http.proxyHost"));
 223         out.println("http.proxyPort :" + System.getProperty("http.proxyPort"));
 224         out.println("https.proxyHost :" + System.getProperty("https.proxyHost"));
 225         out.println("https.proxyPort :" + System.getProperty("https.proxyPort"));
 226         out.println("https.socksProxyHost :"
 227                 + System.getProperty("https.socksProxyHost"));
 228         out.println("https.socksProxyPort :"
 229                 + System.getProperty("https.socksProxyPort"));
 230         out.println("jdk.certpath.disabledAlgorithms :"
 231                 + Security.getProperty("jdk.certpath.disabledAlgorithms"));
 232         out.println("Revocation options :" + certPathChecker.getOptions());
 233         out.println("OCSP responder set :" + certPathChecker.getOcspResponder());
 234         out.println("Trusted root set: " + (trustedRootCerts != null));
 235 
 236         if (validationDate != null) {
 237             out.println("Validation Date:" + validationDate.toString());
 238         }
 239         out.println("Expected EE Status:" + expectedStatus.name());
 240         if (expectedStatus == Status.REVOKED && expectedRevDate != null) {
 241             out.println(
 242                     "Expected EE Revocation Date:" + expectedRevDate.toString());
 243         }
 244         out.println("=====================================================");
 245     }
 246 
 247     private void doCertPathValidate(String[] certsToValidate, PrintStream out)
 248             throws IOException, CertificateException,
 249             InvalidAlgorithmParameterException, ParseException,
 250             NoSuchAlgorithmException, CertPathValidatorException, KeyStoreException {
 251 
 252         if (certsToValidate == null) {
 253             throw new RuntimeException("Require atleast one cert to validate");
 254         }
 255 
 256         // Generate CertPath with certsToValidate
 257         ArrayList<X509Certificate> certs = new ArrayList();
 258         for (String cert : certsToValidate) {
 259             if (cert != null) {
 260                 certs.add(getCertificate(cert));
 261             }
 262         }
 263         CertPath certPath = (CertPath) cf.generateCertPath(certs);
 264 
 265         // Set cacerts as anchor
 266         KeyStore cacerts = KeyStore.getInstance("JKS");
 267         try (FileInputStream fis = new FileInputStream(CACERTS_STORE)) {
 268             cacerts.load(fis, "changeit".toCharArray());
 269         } catch (IOException | NoSuchAlgorithmException | CertificateException ex) {
 270             throw new RuntimeException(ex);
 271         }
 272 
 273         // Set additional trust certificates
 274         if (trustedRootCerts != null) {
 275             for (int i = 0; i < trustedRootCerts.length; i++) {
 276                 X509Certificate rootCACert = getCertificate(trustedRootCerts[i]);
 277                 cacerts.setCertificateEntry("tempca" + i, rootCACert);
 278             }
 279         }
 280 
 281         PKIXParameters params;
 282         params = new PKIXParameters(cacerts);
 283         params.addCertPathChecker(certPathChecker);
 284 
 285         // Set backdated validation if requested, if null, current date is set
 286         params.setDate(validationDate);
 287 
 288         // Validate
 289         certPathValidator.validate(certPath, params);
 290         out.println("Successful CertPath validation");
 291     }
 292 
 293     private X509Certificate getCertificate(String encodedCert)
 294             throws IOException, CertificateException {
 295         ByteArrayInputStream is
 296                 = new ByteArrayInputStream(encodedCert.getBytes());
 297         X509Certificate cert = (X509Certificate) cf.generateCertificate(is);
 298         return cert;
 299     }
 300 
 301     /**
 302      * Set list of disabled algorithms
 303      *
 304      * @param algos algorithms to disable
 305      */
 306     public static void setDisabledAlgorithms(String algos) {
 307         Security.setProperty("jdk.certpath.disabledAlgorithms", algos);
 308     }
 309 
 310     /**
 311      * Enable OCSP only revocation checks, treat network error as success
 312      */
 313     public void enableOCSPCheck() {
 314         // OCSP is by default, disable fallback to CRL
 315         certPathChecker.setOptions(EnumSet.of(
 316                 PKIXRevocationChecker.Option.NO_FALLBACK));
 317     }
 318 
 319     /**
 320      * Enable CRL only revocation check, treat network error as success
 321      */
 322     public void enableCRLCheck() {
 323         certPathChecker.setOptions(EnumSet.of(
 324                 PKIXRevocationChecker.Option.PREFER_CRLS,
 325                 PKIXRevocationChecker.Option.NO_FALLBACK));
 326     }
 327 
 328     /**
 329      * Overrides OCSP responder URL in AIA extension of certificate
 330      *
 331      * @param url OCSP responder
 332      * @throws URISyntaxException
 333      */
 334     public void setOCSPResponderURL(String url) throws URISyntaxException {
 335         certPathChecker.setOcspResponder(new URI(url));
 336     }
 337 
 338     /**
 339      * Set validation date for EE certificate
 340      *
 341      * @param vDate string formatted date
 342      * @throws ParseException if vDate is incorrect
 343      */
 344     public void setValidationDate(String vDate) throws ParseException {
 345         validationDate = DateFormat.getDateInstance(DateFormat.MEDIUM,
 346                 Locale.US).parse(vDate);
 347     }
 348 
 349     /**
 350      * Reset validation date for EE certificate to current date
 351      */
 352     public void resetValidationDate() {
 353         validationDate = null;
 354     }
 355 }