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