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 }