package org.bouncycastle.est;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Pattern;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERPrintableString;
import org.bouncycastle.asn1.cms.ContentInfo;
import org.bouncycastle.asn1.est.CsrAttrs;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cmc.CMCException;
import org.bouncycastle.cmc.SimplePKIResponse;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.util.Selector;
import org.bouncycastle.util.Store;
import org.bouncycastle.util.encoders.Base64;

ESTService provides unified access to an EST server which is defined as implementing RFC7030.
/** * ESTService provides unified access to an EST server which is defined as implementing * RFC7030. */
public class ESTService { protected static final String CACERTS = "/cacerts"; protected static final String SIMPLE_ENROLL = "/simpleenroll"; protected static final String SIMPLE_REENROLL = "/simplereenroll"; protected static final String FULLCMC = "/fullcmc"; protected static final String SERVERGEN = "/serverkeygen"; protected static final String CSRATTRS = "/csrattrs"; protected static final Set<String> illegalParts = new HashSet<String>(); static { illegalParts.add(CACERTS.substring(1)); illegalParts.add(SIMPLE_ENROLL.substring(1)); illegalParts.add(SIMPLE_REENROLL.substring(1)); illegalParts.add(FULLCMC.substring(1)); illegalParts.add(SERVERGEN.substring(1)); illegalParts.add(CSRATTRS.substring(1)); } private final String server; private final ESTClientProvider clientProvider; private static final Pattern pathInvalid = Pattern.compile("^[0-9a-zA-Z_\\-.~!$&'()*+,;=]+"); ESTService( String serverAuthority, String label, ESTClientProvider clientProvider) { serverAuthority = verifyServer(serverAuthority); if (label != null) { label = verifyLabel(label); server = "https://" + serverAuthority + "/.well-known/est/" + label; } else { server = "https://" + serverAuthority + "/.well-known/est"; } this.clientProvider = clientProvider; }
Utility method to extract all the X509Certificates from a store and return them in an array.
Params:
  • store – The store.
Returns:An arrar of certificates/
/** * Utility method to extract all the X509Certificates from a store and return them in an array. * * @param store The store. * @return An arrar of certificates/ */
public static X509CertificateHolder[] storeToArray(Store<X509CertificateHolder> store) { return storeToArray(store, null); }
Utility method to extract all the X509Certificates from a store using a filter and to return them as an array.
Params:
  • store – The store.
  • selector – The selector.
Returns:An array of X509Certificates.
/** * Utility method to extract all the X509Certificates from a store using a filter and to return them * as an array. * * @param store The store. * @param selector The selector. * @return An array of X509Certificates. */
public static X509CertificateHolder[] storeToArray(Store<X509CertificateHolder> store, Selector<X509CertificateHolder> selector) { Collection<X509CertificateHolder> c = store.getMatches(selector); return c.toArray(new X509CertificateHolder[c.size()]); }
Query the EST server for ca certificates.

RFC7030 leans heavily on the verification phases of TLS for both client and server verification.

It does however define a bootstrapping mode where if the client does not have the necessary ca certificates to validate the server it can defer to an external source, such as a human, to formally accept the ca certs.

If callers are using bootstrapping they must examine the CACertsResponse and validate it externally.

Returns:A store of X509Certificates.
/** * Query the EST server for ca certificates. * <p> * RFC7030 leans heavily on the verification phases of TLS for both client and server verification. * <p> * It does however define a bootstrapping mode where if the client does not have the necessary ca certificates to * validate the server it can defer to an external source, such as a human, to formally accept the ca certs. * <p> * If callers are using bootstrapping they must examine the CACertsResponse and validate it externally. * * @return A store of X509Certificates. */
public CACertsResponse getCACerts() throws Exception { ESTResponse resp = null; Exception finalThrowable = null; CACertsResponse caCertsResponse = null; URL url = null; boolean failedBeforeClose = false; try { url = new URL(server + CACERTS); ESTClient client = clientProvider.makeClient(); ESTRequest req = new ESTRequestBuilder("GET", url).withClient(client).build(); resp = client.doRequest(req); Store<X509CertificateHolder> caCerts = null; Store<X509CRLHolder> crlHolderStore = null; if (resp.getStatusCode() == 200) { if (!"application/pkcs7-mime".equals(resp.getHeaders().getFirstValue("Content-Type"))) { String j = resp.getHeaders().getFirstValue("Content-Type") != null ? " got " + resp.getHeaders().getFirstValue("Content-Type") : " but was not present."; throw new ESTException(("Response : " + url.toString() + "Expecting application/pkcs7-mime ") + j, null, resp.getStatusCode(), resp.getInputStream()); } try { if (resp.getContentLength() != null && resp.getContentLength() > 0) { ASN1InputStream ain = new ASN1InputStream(resp.getInputStream()); SimplePKIResponse spkr = new SimplePKIResponse(ContentInfo.getInstance((ASN1Sequence)ain.readObject())); caCerts = spkr.getCertificates(); crlHolderStore = spkr.getCRLs(); } } catch (Throwable ex) { throw new ESTException("Decoding CACerts: " + url.toString() + " " + ex.getMessage(), ex, resp.getStatusCode(), resp.getInputStream()); } } else if (resp.getStatusCode() != 204) // 204 are No Content { throw new ESTException("Get CACerts: " + url.toString(), null, resp.getStatusCode(), resp.getInputStream()); } caCertsResponse = new CACertsResponse(caCerts, crlHolderStore, req, resp.getSource(), clientProvider.isTrusted()); } catch (Throwable t) { failedBeforeClose = true; if (t instanceof ESTException) { throw (ESTException)t; } else { throw new ESTException(t.getMessage(), t); } } finally { if (resp != null) { try { resp.close(); } catch (Exception t) { finalThrowable = t; } } } if (finalThrowable != null) { if (finalThrowable instanceof ESTException) { throw finalThrowable; } throw new ESTException("Get CACerts: " + url.toString(), finalThrowable, resp.getStatusCode(), null); } return caCertsResponse; }
Reissue an existing request where the server had previously returned a 202.
Params:
  • priorResponse – The prior response.
Throws:
Returns:A new ESTEnrollmentResponse
/** * Reissue an existing request where the server had previously returned a 202. * * @param priorResponse The prior response. * @return A new ESTEnrollmentResponse * @throws Exception */
public EnrollmentResponse simpleEnroll(EnrollmentResponse priorResponse) throws Exception { if (!clientProvider.isTrusted()) { throw new IllegalStateException("No trust anchors."); } ESTResponse resp = null; try { ESTClient client = clientProvider.makeClient(); resp = client.doRequest(new ESTRequestBuilder(priorResponse.getRequestToRetry()).withClient(client).build()); return handleEnrollResponse(resp); } catch (Throwable t) { if (t instanceof ESTException) { throw (ESTException)t; } else { throw new ESTException(t.getMessage(), t); } } finally { if (resp != null) { resp.close(); } } }
Perform a simple enrollment operation.

This method accepts an ESPHttpAuth instance to provide basic or digest authentication.

If authentication is to be performed as part of TLS then this instances client keystore and their keystore password need to be specified.

Params:
  • certificationRequest – The certification request.
  • auth – The http auth provider, basic auth or digest auth, can be null.
Returns:The enrolled certificate.
/** * Perform a simple enrollment operation. * <p> * This method accepts an ESPHttpAuth instance to provide basic or digest authentication. * <p> * If authentication is to be performed as part of TLS then this instances client keystore and their keystore * password need to be specified. * * @param certificationRequest The certification request. * @param auth The http auth provider, basic auth or digest auth, can be null. * @return The enrolled certificate. */
public EnrollmentResponse simpleEnroll(boolean reenroll, PKCS10CertificationRequest certificationRequest, ESTAuth auth) throws IOException { if (!clientProvider.isTrusted()) { throw new IllegalStateException("No trust anchors."); } ESTResponse resp = null; try { final byte[] data = annotateRequest(certificationRequest.getEncoded()).getBytes(); URL url = new URL(server + (reenroll ? SIMPLE_REENROLL : SIMPLE_ENROLL)); ESTClient client = clientProvider.makeClient(); ESTRequestBuilder req = new ESTRequestBuilder("POST", url).withData(data).withClient(client); req.addHeader("Content-Type", "application/pkcs10"); req.addHeader("Content-Length", "" + data.length); req.addHeader("Content-Transfer-Encoding", "base64"); if (auth != null) { auth.applyAuth(req); } resp = client.doRequest(req.build()); return handleEnrollResponse(resp); } catch (Throwable t) { if (t instanceof ESTException) { throw (ESTException)t; } else { throw new ESTException(t.getMessage(), t); } } finally { if (resp != null) { resp.close(); } } }
Implements Enroll with PoP. Request will have the tls-unique attribute added to it before it is signed and completed.
Params:
  • reEnroll – True = re enroll.
  • builder – The request builder.
  • contentSigner – The content signer.
  • auth – Auth modes.
Throws:
Returns:Enrollment response.
/** * Implements Enroll with PoP. * Request will have the tls-unique attribute added to it before it is signed and completed. * * @param reEnroll True = re enroll. * @param builder The request builder. * @param contentSigner The content signer. * @param auth Auth modes. * @return Enrollment response. * @throws IOException */
public EnrollmentResponse simpleEnrollPoP(boolean reEnroll, final PKCS10CertificationRequestBuilder builder, final ContentSigner contentSigner, ESTAuth auth) throws IOException { if (!clientProvider.isTrusted()) { throw new IllegalStateException("No trust anchors."); } ESTResponse resp = null; try { URL url = new URL(server + (reEnroll ? SIMPLE_REENROLL : SIMPLE_ENROLL)); ESTClient client = clientProvider.makeClient(); // // Connect supplying a source listener. // The source listener is responsible for completing the PCS10 Cert request and encoding it. // ESTRequestBuilder reqBldr = new ESTRequestBuilder("POST", url).withClient(client).withConnectionListener(new ESTSourceConnectionListener() { public ESTRequest onConnection(Source source, ESTRequest request) throws IOException { // // Add challenge password from tls unique // if (source instanceof TLSUniqueProvider && ((TLSUniqueProvider)source).isTLSUniqueAvailable()) { PKCS10CertificationRequestBuilder localBuilder = new PKCS10CertificationRequestBuilder(builder); ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] tlsUnique = ((TLSUniqueProvider)source).getTLSUnique(); localBuilder.setAttribute(PKCSObjectIdentifiers.pkcs_9_at_challengePassword, new DERPrintableString(Base64.toBase64String(tlsUnique))); bos.write(annotateRequest(localBuilder.build(contentSigner).getEncoded()).getBytes()); bos.flush(); ESTRequestBuilder reqBuilder = new ESTRequestBuilder(request).withData(bos.toByteArray()); reqBuilder.setHeader("Content-Type", "application/pkcs10"); reqBuilder.setHeader("Content-Transfer-Encoding", "base64"); reqBuilder.setHeader("Content-Length", Long.toString(bos.size())); return reqBuilder.build(); } else { throw new IOException("Source does not supply TLS unique."); } } }); if (auth != null) { auth.applyAuth(reqBldr); } resp = client.doRequest(reqBldr.build()); return handleEnrollResponse(resp); } catch (Throwable t) { if (t instanceof ESTException) { throw (ESTException)t; } else { throw new ESTException(t.getMessage(), t); } } finally { if (resp != null) { resp.close(); } } }
Handles the enroll response, deals with status codes and setting of delays.
Params:
  • resp – The response.
Throws:
Returns:An EnrollmentResponse.
/** * Handles the enroll response, deals with status codes and setting of delays. * * @param resp The response. * @return An EnrollmentResponse. * @throws IOException */
protected EnrollmentResponse handleEnrollResponse(ESTResponse resp) throws IOException { ESTRequest req = resp.getOriginalRequest(); Store<X509CertificateHolder> enrolled = null; if (resp.getStatusCode() == 202) { // Received but not ready. String rt = resp.getHeader("Retry-After"); if (rt == null) { throw new ESTException("Got Status 202 but not Retry-After header from: " + req.getURL().toString()); } long notBefore = -1; try { notBefore = System.currentTimeMillis() + (Long.parseLong(rt) * 1000); } catch (NumberFormatException nfe) { try { SimpleDateFormat dateFormat = new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss z", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); notBefore = dateFormat.parse(rt).getTime(); } catch (Exception ex) { throw new ESTException( "Unable to parse Retry-After header:" + req.getURL().toString() + " " + ex.getMessage(), null, resp.getStatusCode(), resp.getInputStream()); } } return new EnrollmentResponse(null, notBefore, req, resp.getSource()); } else if (resp.getStatusCode() == 200) { ASN1InputStream ain = new ASN1InputStream(resp.getInputStream()); SimplePKIResponse spkr = null; try { spkr = new SimplePKIResponse(ContentInfo.getInstance(ain.readObject())); } catch (CMCException e) { throw new ESTException(e.getMessage(), e.getCause()); } enrolled = spkr.getCertificates(); return new EnrollmentResponse(enrolled, -1, null, resp.getSource()); } throw new ESTException( "Simple Enroll: " + req.getURL().toString(), null, resp.getStatusCode(), resp.getInputStream()); }
Fetch he CSR Attributes from the server.
Throws:
Returns:A CSRRequestResponse with the attributes.
/** * Fetch he CSR Attributes from the server. * * @return A CSRRequestResponse with the attributes. * @throws ESTException */
public CSRRequestResponse getCSRAttributes() throws ESTException { if (!clientProvider.isTrusted()) { throw new IllegalStateException("No trust anchors."); } ESTResponse resp = null; CSRAttributesResponse response = null; Exception finalThrowable = null; URL url = null; try { url = new URL(server + CSRATTRS); ESTClient client = clientProvider.makeClient(); ESTRequest req = new ESTRequestBuilder("GET", url).withClient(client).build(); // new ESTRequest("GET", url, null); resp = client.doRequest(req); switch (resp.getStatusCode()) { case 200: try { if (resp.getContentLength() != null && resp.getContentLength() > 0) { ASN1InputStream ain = new ASN1InputStream(resp.getInputStream()); ASN1Sequence seq = (ASN1Sequence)ain.readObject(); response = new CSRAttributesResponse(CsrAttrs.getInstance(seq)); } } catch (Throwable ex) { throw new ESTException("Decoding CACerts: " + url.toString() + " " + ex.getMessage(), ex, resp.getStatusCode(), resp.getInputStream()); } break; case 204: response = null; break; case 404: response = null; break; default: throw new ESTException( "CSR Attribute request: " + req.getURL().toString(), null, resp.getStatusCode(), resp.getInputStream()); } } catch (Throwable t) { if (t instanceof ESTException) { throw (ESTException)t; } else { throw new ESTException(t.getMessage(), t); } } finally { if (resp != null) { try { resp.close(); } catch (Exception ex) { finalThrowable = ex; } } } if (finalThrowable != null) { if (finalThrowable instanceof ESTException) { throw (ESTException)finalThrowable; } throw new ESTException(finalThrowable.getMessage(), finalThrowable, resp.getStatusCode(), null); } return new CSRRequestResponse(response, resp.getSource()); } private String annotateRequest(byte[] data) { int i = 0; StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); // pw.print("-----BEGIN CERTIFICATE REQUEST-----\n"); do { if (i + 48 < data.length) { pw.print(Base64.toBase64String(data, i, 48)); i += 48; } else { pw.print(Base64.toBase64String(data, i, data.length - i)); i = data.length; } pw.print('\n'); } while (i < data.length); // pw.print("-----END CERTIFICATE REQUEST-----\n"); pw.flush(); return sw.toString(); } private String verifyLabel(String label) { while (label.endsWith("/") && label.length() > 0) { label = label.substring(0, label.length() - 1); } while (label.startsWith("/") && label.length() > 0) { label = label.substring(1); } if (label.length() == 0) { throw new IllegalArgumentException("Label set but after trimming '/' is not zero length string."); } if (!pathInvalid.matcher(label).matches()) { throw new IllegalArgumentException("Server path " + label + " contains invalid characters"); } if (illegalParts.contains(label)) { throw new IllegalArgumentException("Label " + label + " is a reserved path segment."); } return label; } private String verifyServer(String server) { try { while (server.endsWith("/") && server.length() > 0) { server = server.substring(0, server.length() - 1); } if (server.contains("://")) { throw new IllegalArgumentException("Server contains scheme, must only be <dnsname/ipaddress>:port, https:// will be added arbitrarily."); } URL u = new URL("https://" + server); if (u.getPath().length() == 0 || u.getPath().equals("/")) { return server; } throw new IllegalArgumentException("Server contains path, must only be <dnsname/ipaddress>:port, a path of '/.well-known/est/<label>' will be added arbitrarily."); } catch (Exception ex) { if (ex instanceof IllegalArgumentException) { throw (IllegalArgumentException)ex; } throw new IllegalArgumentException("Scheme and host is invalid: " + ex.getMessage(), ex); } } }