1 package org.bouncycastle.est; 2 3 import java.io.ByteArrayOutputStream; 4 import java.io.IOException; 5 import java.io.PrintWriter; 6 import java.io.StringWriter; 7 import java.net.URL; 8 import java.text.SimpleDateFormat; 9 import java.util.Collection; 10 import java.util.HashSet; 11 import java.util.Locale; 12 import java.util.Set; 13 import java.util.TimeZone; 14 import java.util.regex.Pattern; 15 16 import org.bouncycastle.asn1.ASN1InputStream; 17 import org.bouncycastle.asn1.ASN1Sequence; 18 import org.bouncycastle.asn1.DERPrintableString; 19 import org.bouncycastle.asn1.cms.ContentInfo; 20 import org.bouncycastle.asn1.est.CsrAttrs; 21 import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; 22 import org.bouncycastle.cert.X509CRLHolder; 23 import org.bouncycastle.cert.X509CertificateHolder; 24 import org.bouncycastle.cmc.CMCException; 25 import org.bouncycastle.cmc.SimplePKIResponse; 26 import org.bouncycastle.operator.ContentSigner; 27 import org.bouncycastle.pkcs.PKCS10CertificationRequest; 28 import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; 29 import org.bouncycastle.util.Selector; 30 import org.bouncycastle.util.Store; 31 import org.bouncycastle.util.encoders.Base64; 32 33 /** 34 * ESTService provides unified access to an EST server which is defined as implementing 35 * RFC7030. 36 */ 37 public class ESTService 38 { 39 protected static final String CACERTS = "/cacerts"; 40 protected static final String SIMPLE_ENROLL = "/simpleenroll"; 41 protected static final String SIMPLE_REENROLL = "/simplereenroll"; 42 protected static final String FULLCMC = "/fullcmc"; 43 protected static final String SERVERGEN = "/serverkeygen"; 44 protected static final String CSRATTRS = "/csrattrs"; 45 46 protected static final Set<String> illegalParts = new HashSet<String>(); 47 48 static 49 { 50 illegalParts.add(CACERTS.substring(1)); 51 illegalParts.add(SIMPLE_ENROLL.substring(1)); 52 illegalParts.add(SIMPLE_REENROLL.substring(1)); 53 illegalParts.add(FULLCMC.substring(1)); 54 illegalParts.add(SERVERGEN.substring(1)); 55 illegalParts.add(CSRATTRS.substring(1)); 56 } 57 58 59 private final String server; 60 private final ESTClientProvider clientProvider; 61 62 private static final Pattern pathInValid = Pattern.compile("^[0-9a-zA-Z_\\-.~!$&'()*+,;:=]+"); 63 ESTService( String serverAuthority, String label, ESTClientProvider clientProvider)64 ESTService( 65 String serverAuthority, String label, 66 ESTClientProvider clientProvider) 67 { 68 69 serverAuthority = verifyServer(serverAuthority); 70 71 if (label != null) 72 { 73 label = verifyLabel(label); 74 server = "https://" + serverAuthority + "/.well-known/est/" + label; 75 } 76 else 77 { 78 server = "https://" + serverAuthority + "/.well-known/est"; 79 } 80 81 this.clientProvider = clientProvider; 82 } 83 84 /** 85 * Utility method to extract all the X509Certificates from a store and return them in an array. 86 * 87 * @param store The store. 88 * @return An arrar of certificates/ 89 */ storeToArray(Store<X509CertificateHolder> store)90 public static X509CertificateHolder[] storeToArray(Store<X509CertificateHolder> store) 91 { 92 return storeToArray(store, null); 93 } 94 95 /** 96 * Utility method to extract all the X509Certificates from a store using a filter and to return them 97 * as an array. 98 * 99 * @param store The store. 100 * @param selector The selector. 101 * @return An array of X509Certificates. 102 */ storeToArray(Store<X509CertificateHolder> store, Selector<X509CertificateHolder> selector)103 public static X509CertificateHolder[] storeToArray(Store<X509CertificateHolder> store, Selector<X509CertificateHolder> selector) 104 { 105 Collection<X509CertificateHolder> c = store.getMatches(selector); 106 return c.toArray(new X509CertificateHolder[c.size()]); 107 } 108 109 /** 110 * Query the EST server for ca certificates. 111 * <p> 112 * RFC7030 leans heavily on the verification phases of TLS for both client and server verification. 113 * <p> 114 * It does however define a bootstrapping mode where if the client does not have the necessary ca certificates to 115 * validate the server it can defer to an external source, such as a human, to formally accept the ca certs. 116 * <p> 117 * If callers are using bootstrapping they must examine the CACertsResponse and validate it externally. 118 * 119 * @return A store of X509Certificates. 120 */ getCACerts()121 public CACertsResponse getCACerts() 122 throws ESTException 123 { 124 ESTResponse resp = null; 125 Exception finalThrowable = null; 126 CACertsResponse caCertsResponse = null; 127 URL url = null; 128 boolean failedBeforeClose = false; 129 try 130 { 131 url = new URL(server + CACERTS); 132 133 ESTClient client = clientProvider.makeClient(); 134 ESTRequest req = new ESTRequestBuilder("GET", url).withClient(client).build(); 135 resp = client.doRequest(req); 136 137 Store<X509CertificateHolder> caCerts = null; 138 Store<X509CRLHolder> crlHolderStore = null; 139 140 if (resp.getStatusCode() == 200) 141 { 142 String contentType = resp.getHeaders().getFirstValue("Content-Type"); 143 if (contentType == null || !contentType.startsWith("application/pkcs7-mime")) 144 { 145 String j = contentType != null ? " got " + contentType : " but was not present."; 146 throw new ESTException(("Response : " + url.toString() + "Expecting application/pkcs7-mime ") + j, null, resp.getStatusCode(), resp.getInputStream()); 147 } 148 149 try 150 { 151 if (resp.getContentLength() != null && resp.getContentLength() > 0) 152 { 153 ASN1InputStream ain = new ASN1InputStream(resp.getInputStream()); 154 SimplePKIResponse spkr = new SimplePKIResponse(ContentInfo.getInstance((ASN1Sequence)ain.readObject())); 155 caCerts = spkr.getCertificates(); 156 crlHolderStore = spkr.getCRLs(); 157 } 158 } 159 catch (Throwable ex) 160 { 161 throw new ESTException("Decoding CACerts: " + url.toString() + " " + ex.getMessage(), ex, resp.getStatusCode(), resp.getInputStream()); 162 } 163 164 } 165 else if (resp.getStatusCode() != 204) // 204 are No Content 166 { 167 throw new ESTException("Get CACerts: " + url.toString(), null, resp.getStatusCode(), resp.getInputStream()); 168 } 169 170 caCertsResponse = new CACertsResponse(caCerts, crlHolderStore, req, resp.getSource(), clientProvider.isTrusted()); 171 172 } 173 catch (Throwable t) 174 { 175 failedBeforeClose = true; 176 if (t instanceof ESTException) 177 { 178 throw (ESTException)t; 179 } 180 else 181 { 182 throw new ESTException(t.getMessage(), t); 183 } 184 } 185 finally 186 { 187 if (resp != null) 188 { 189 try 190 { 191 resp.close(); 192 } 193 catch (Exception t) 194 { 195 finalThrowable = t; 196 } 197 } 198 } 199 200 if (finalThrowable != null) 201 { 202 if (finalThrowable instanceof ESTException) 203 { 204 throw (ESTException)finalThrowable; 205 } 206 throw new ESTException("Get CACerts: " + url.toString(), finalThrowable, resp.getStatusCode(), null); 207 } 208 209 return caCertsResponse; 210 } 211 212 /** 213 * Reissue an existing request where the server had previously returned a 202. 214 * 215 * @param priorResponse The prior response. 216 * @return A new ESTEnrollmentResponse 217 * @throws Exception 218 */ simpleEnroll(EnrollmentResponse priorResponse)219 public EnrollmentResponse simpleEnroll(EnrollmentResponse priorResponse) 220 throws Exception 221 { 222 if (!clientProvider.isTrusted()) 223 { 224 throw new IllegalStateException("No trust anchors."); 225 } 226 227 ESTResponse resp = null; 228 229 try 230 { 231 ESTClient client = clientProvider.makeClient(); 232 resp = client.doRequest(new ESTRequestBuilder(priorResponse.getRequestToRetry()).withClient(client).build()); 233 return handleEnrollResponse(resp); 234 } 235 catch (Throwable t) 236 { 237 if (t instanceof ESTException) 238 { 239 throw (ESTException)t; 240 } 241 else 242 { 243 throw new ESTException(t.getMessage(), t); 244 } 245 } 246 finally 247 { 248 if (resp != null) 249 { 250 resp.close(); 251 } 252 } 253 } 254 255 /** 256 * Perform a simple enrollment operation. 257 * <p> 258 * This method accepts an ESPHttpAuth instance to provide basic or digest authentication. 259 * <p> 260 * If authentication is to be performed as part of TLS then this instances client keystore and their keystore 261 * password need to be specified. 262 * 263 * @param certificationRequest The certification request. 264 * @param auth The http auth provider, basic auth or digest auth, can be null. 265 * @return The enrolled certificate. 266 */ simpleEnroll(boolean reenroll, PKCS10CertificationRequest certificationRequest, ESTAuth auth)267 public EnrollmentResponse simpleEnroll(boolean reenroll, PKCS10CertificationRequest certificationRequest, ESTAuth auth) 268 throws IOException 269 { 270 if (!clientProvider.isTrusted()) 271 { 272 throw new IllegalStateException("No trust anchors."); 273 } 274 275 ESTResponse resp = null; 276 try 277 { 278 final byte[] data = annotateRequest(certificationRequest.getEncoded()).getBytes(); 279 280 URL url = new URL(server + (reenroll ? SIMPLE_REENROLL : SIMPLE_ENROLL)); 281 282 283 ESTClient client = clientProvider.makeClient(); 284 ESTRequestBuilder req = new ESTRequestBuilder("POST", url).withData(data).withClient(client); 285 286 req.addHeader("Content-Type", "application/pkcs10"); 287 req.addHeader("Content-Length", "" + data.length); 288 req.addHeader("Content-Transfer-Encoding", "base64"); 289 290 if (auth != null) 291 { 292 auth.applyAuth(req); 293 } 294 295 resp = client.doRequest(req.build()); 296 297 return handleEnrollResponse(resp); 298 299 } 300 catch (Throwable t) 301 { 302 if (t instanceof ESTException) 303 { 304 throw (ESTException)t; 305 } 306 else 307 { 308 throw new ESTException(t.getMessage(), t); 309 } 310 } 311 finally 312 { 313 if (resp != null) 314 { 315 resp.close(); 316 } 317 } 318 319 } 320 321 322 /** 323 * Implements Enroll with PoP. 324 * Request will have the tls-unique attribute added to it before it is signed and completed. 325 * 326 * @param reEnroll True = re enroll. 327 * @param builder The request builder. 328 * @param contentSigner The content signer. 329 * @param auth Auth modes. 330 * @return Enrollment response. 331 * @throws IOException 332 */ simpleEnrollPoP(boolean reEnroll, final PKCS10CertificationRequestBuilder builder, final ContentSigner contentSigner, ESTAuth auth)333 public EnrollmentResponse simpleEnrollPoP(boolean reEnroll, final PKCS10CertificationRequestBuilder builder, final ContentSigner contentSigner, ESTAuth auth) 334 throws IOException 335 { 336 if (!clientProvider.isTrusted()) 337 { 338 throw new IllegalStateException("No trust anchors."); 339 } 340 341 ESTResponse resp = null; 342 try 343 { 344 URL url = new URL(server + (reEnroll ? SIMPLE_REENROLL : SIMPLE_ENROLL)); 345 ESTClient client = clientProvider.makeClient(); 346 347 // 348 // Connect supplying a source listener. 349 // The source listener is responsible for completing the PCS10 Cert request and encoding it. 350 // 351 ESTRequestBuilder reqBldr = new ESTRequestBuilder("POST", url).withClient(client).withConnectionListener(new ESTSourceConnectionListener() 352 { 353 public ESTRequest onConnection(Source source, ESTRequest request) 354 throws IOException 355 { 356 // 357 // Add challenge password from tls unique 358 // 359 360 if (source instanceof TLSUniqueProvider && ((TLSUniqueProvider)source).isTLSUniqueAvailable()) 361 { 362 PKCS10CertificationRequestBuilder localBuilder = new PKCS10CertificationRequestBuilder(builder); 363 364 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 365 byte[] tlsUnique = ((TLSUniqueProvider)source).getTLSUnique(); 366 367 localBuilder.setAttribute(PKCSObjectIdentifiers.pkcs_9_at_challengePassword, new DERPrintableString(Base64.toBase64String(tlsUnique))); 368 bos.write(annotateRequest(localBuilder.build(contentSigner).getEncoded()).getBytes()); 369 bos.flush(); 370 371 ESTRequestBuilder reqBuilder = new ESTRequestBuilder(request).withData(bos.toByteArray()); 372 373 reqBuilder.setHeader("Content-Type", "application/pkcs10"); 374 reqBuilder.setHeader("Content-Transfer-Encoding", "base64"); 375 reqBuilder.setHeader("Content-Length", Long.toString(bos.size())); 376 377 return reqBuilder.build(); 378 } 379 else 380 { 381 throw new IOException("Source does not supply TLS unique."); 382 } 383 } 384 }); 385 386 if (auth != null) 387 { 388 auth.applyAuth(reqBldr); 389 } 390 391 resp = client.doRequest(reqBldr.build()); 392 393 return handleEnrollResponse(resp); 394 395 } 396 catch (Throwable t) 397 { 398 if (t instanceof ESTException) 399 { 400 throw (ESTException)t; 401 } 402 else 403 { 404 throw new ESTException(t.getMessage(), t); 405 } 406 } 407 finally 408 { 409 if (resp != null) 410 { 411 resp.close(); 412 } 413 } 414 415 } 416 417 418 /** 419 * Handles the enroll response, deals with status codes and setting of delays. 420 * 421 * @param resp The response. 422 * @return An EnrollmentResponse. 423 * @throws IOException 424 */ handleEnrollResponse(ESTResponse resp)425 protected EnrollmentResponse handleEnrollResponse(ESTResponse resp) 426 throws IOException 427 { 428 429 ESTRequest req = resp.getOriginalRequest(); 430 Store<X509CertificateHolder> enrolled = null; 431 if (resp.getStatusCode() == 202) 432 { 433 // Received but not ready. 434 String rt = resp.getHeader("Retry-After"); 435 436 if (rt == null) 437 { 438 throw new ESTException("Got Status 202 but not Retry-After header from: " + req.getURL().toString()); 439 } 440 441 long notBefore = -1; 442 443 444 try 445 { 446 notBefore = System.currentTimeMillis() + (Long.parseLong(rt) * 1000); 447 } 448 catch (NumberFormatException nfe) 449 { 450 try 451 { 452 SimpleDateFormat dateFormat = new SimpleDateFormat( 453 "EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 454 dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); 455 notBefore = dateFormat.parse(rt).getTime(); 456 } 457 catch (Exception ex) 458 { 459 throw new ESTException( 460 "Unable to parse Retry-After header:" + req.getURL().toString() + " " + ex.getMessage(), null, 461 resp.getStatusCode(), resp.getInputStream()); 462 } 463 } 464 465 return new EnrollmentResponse(null, notBefore, req, resp.getSource()); 466 467 } 468 else if (resp.getStatusCode() == 200) 469 { 470 ASN1InputStream ain = new ASN1InputStream(resp.getInputStream()); 471 SimplePKIResponse spkr = null; 472 try 473 { 474 spkr = new SimplePKIResponse(ContentInfo.getInstance(ain.readObject())); 475 } 476 catch (CMCException e) 477 { 478 throw new ESTException(e.getMessage(), e.getCause()); 479 } 480 enrolled = spkr.getCertificates(); 481 return new EnrollmentResponse(enrolled, -1, null, resp.getSource()); 482 } 483 484 throw new ESTException( 485 "Simple Enroll: " + req.getURL().toString(), null, 486 resp.getStatusCode(), resp.getInputStream()); 487 488 } 489 490 /** 491 * Fetch he CSR Attributes from the server. 492 * 493 * @return A CSRRequestResponse with the attributes. 494 * @throws ESTException 495 */ getCSRAttributes()496 public CSRRequestResponse getCSRAttributes() 497 throws ESTException 498 { 499 500 if (!clientProvider.isTrusted()) 501 { 502 throw new IllegalStateException("No trust anchors."); 503 } 504 505 ESTResponse resp = null; 506 CSRAttributesResponse response = null; 507 Exception finalThrowable = null; 508 URL url = null; 509 try 510 { 511 url = new URL(server + CSRATTRS); 512 513 ESTClient client = clientProvider.makeClient(); 514 ESTRequest req = new ESTRequestBuilder("GET", url).withClient(client).build(); // new ESTRequest("GET", url, null); 515 resp = client.doRequest(req); 516 517 518 switch (resp.getStatusCode()) 519 { 520 case 200: 521 try 522 { 523 if (resp.getContentLength() != null && resp.getContentLength() > 0) 524 { 525 ASN1InputStream ain = new ASN1InputStream(resp.getInputStream()); 526 ASN1Sequence seq = ASN1Sequence.getInstance(ain.readObject()); 527 response = new CSRAttributesResponse(CsrAttrs.getInstance(seq)); 528 } 529 } 530 catch (Throwable ex) 531 { 532 throw new ESTException("Decoding CACerts: " + url.toString() + " " + ex.getMessage(), ex, resp.getStatusCode(), resp.getInputStream()); 533 } 534 535 break; 536 case 204: 537 response = null; 538 break; 539 case 404: 540 response = null; 541 break; 542 default: 543 throw new ESTException( 544 "CSR Attribute request: " + req.getURL().toString(), null, 545 resp.getStatusCode(), resp.getInputStream()); 546 } 547 } 548 catch (Throwable t) 549 { 550 551 if (t instanceof ESTException) 552 { 553 throw (ESTException)t; 554 } 555 else 556 { 557 throw new ESTException(t.getMessage(), t); 558 } 559 } 560 finally 561 { 562 if (resp != null) 563 { 564 try 565 { 566 resp.close(); 567 } 568 catch (Exception ex) 569 { 570 finalThrowable = ex; 571 } 572 } 573 } 574 575 if (finalThrowable != null) 576 { 577 if (finalThrowable instanceof ESTException) 578 { 579 throw (ESTException)finalThrowable; 580 } 581 throw new ESTException(finalThrowable.getMessage(), finalThrowable, resp.getStatusCode(), null); 582 } 583 584 return new CSRRequestResponse(response, resp.getSource()); 585 } 586 annotateRequest(byte[] data)587 private String annotateRequest(byte[] data) 588 { 589 int i = 0; 590 StringWriter sw = new StringWriter(); 591 PrintWriter pw = new PrintWriter(sw); 592 // pw.print("-----BEGIN CERTIFICATE REQUEST-----\n"); 593 do 594 { 595 if (i + 48 < data.length) 596 { 597 pw.print(Base64.toBase64String(data, i, 48)); 598 i += 48; 599 } 600 else 601 { 602 pw.print(Base64.toBase64String(data, i, data.length - i)); 603 i = data.length; 604 } 605 pw.print('\n'); 606 } 607 while (i < data.length); 608 // pw.print("-----END CERTIFICATE REQUEST-----\n"); 609 pw.flush(); 610 return sw.toString(); 611 } 612 613 verifyLabel(String label)614 private String verifyLabel(String label) 615 { 616 while (label.endsWith("/") && label.length() > 0) 617 { 618 label = label.substring(0, label.length() - 1); 619 } 620 621 while (label.startsWith("/") && label.length() > 0) 622 { 623 label = label.substring(1); 624 } 625 626 if (label.length() == 0) 627 { 628 throw new IllegalArgumentException("Label set but after trimming '/' is not zero length string."); 629 } 630 631 if (!pathInValid.matcher(label).matches()) 632 { 633 throw new IllegalArgumentException("Server path " + label + " contains invalid characters"); 634 } 635 636 if (illegalParts.contains(label)) 637 { 638 throw new IllegalArgumentException("Label " + label + " is a reserved path segment."); 639 } 640 641 return label; 642 643 } 644 645 verifyServer(String server)646 private String verifyServer(String server) 647 { 648 try 649 { 650 651 while (server.endsWith("/") && server.length() > 0) 652 { 653 server = server.substring(0, server.length() - 1); 654 } 655 656 if (server.contains("://")) 657 { 658 throw new IllegalArgumentException("Server contains scheme, must only be <dnsname/ipaddress>:port, https:// will be added arbitrarily."); 659 } 660 661 URL u = new URL("https://" + server); 662 if (u.getPath().length() == 0 || u.getPath().equals("/")) 663 { 664 return server; 665 } 666 667 throw new IllegalArgumentException("Server contains path, must only be <dnsname/ipaddress>:port, a path of '/.well-known/est/<label>' will be added arbitrarily."); 668 669 } 670 catch (Exception ex) 671 { 672 if (ex instanceof IllegalArgumentException) 673 { 674 throw (IllegalArgumentException)ex; 675 } 676 throw new IllegalArgumentException("Scheme and host is invalid: " + ex.getMessage(), ex); 677 } 678 679 } 680 } 681