Project

General

Profile

3240b_1.txt

Sergey Ivanovskiy, 10/31/2017 09:15 AM

Download (35.5 KB)

 
1
=== modified file 'build.gradle'
2
--- build.gradle	2017-07-14 21:57:43 +0000
3
+++ build.gradle	2017-10-30 23:47:56 +0000
4
@@ -312,6 +312,8 @@
5
     fwdClientServer group: 'org.apache.axis2', name: 'axis2-adb', version: '1.6.2'
6
     fwdClientServer group: 'javax.mail', name: 'mail', version: '1.4'
7
     fwdClientServer group: 'args4j', name: 'args4j', version: '2.33'
8
+    fwdClientServer group: 'org.shredzone.acme4j', name: 'acme4j-client', version: '0.10'
9
+    fwdClientServer group: 'org.shredzone.acme4j', name: 'acme4j-utils', version: '0.10'
10
 
11
     fwdServer group: 'org.postgresql', name: 'postgresql', version: '9.4.1211'
12
     fwdServer group: 'org.hibernate', name: 'hibernate-c3p0', version: '4.1.8.Final'
13

    
14
=== added file 'src/com/goldencode/p2j/security/AcmeClient.java'
15
--- src/com/goldencode/p2j/security/AcmeClient.java	1970-01-01 00:00:00 +0000
16
+++ src/com/goldencode/p2j/security/AcmeClient.java	2017-10-31 12:51:57 +0000
17
@@ -0,0 +1,709 @@
18
+/*
19
+** Module   : AcmeClient.java
20
+** Abstract : Implements ACME client to get certificates signed by well-known trusted authority.
21
+**
22
+** Copyright (c) 2017, Golden Code Development Corporation.
23
+**
24
+** -#- -I- --Date-- ---------------------------------Description----------------------------------
25
+** 001 SBI 20171030 First version.
26
+*/
27
+/*
28
+** This program is free software: you can redistribute it and/or modify
29
+** it under the terms of the GNU Affero General Public License as
30
+** published by the Free Software Foundation, either version 3 of the
31
+** License, or (at your option) any later version.
32
+**
33
+** This program is distributed in the hope that it will be useful,
34
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
35
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
36
+** GNU Affero General Public License for more details.
37
+**
38
+** You may find a copy of the GNU Affero GPL version 3 at the following
39
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
40
+** 
41
+** Additional terms under GNU Affero GPL version 3 section 7:
42
+** 
43
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
44
+**   terms apply to the works covered under the License.  These additional terms
45
+**   are non-permissive additional terms allowed under Section 7 of the GNU
46
+**   Affero GPL version 3 and may not be removed by you.
47
+** 
48
+**   0. Attribution Requirement.
49
+** 
50
+**     You must preserve all legal notices or author attributions in the covered
51
+**     work or Appropriate Legal Notices displayed by works containing the covered
52
+**     work.  You may not remove from the covered work any author or developer
53
+**     credit already included within the covered work.
54
+** 
55
+**   1. No License To Use Trademarks.
56
+** 
57
+**     This license does not grant any license or rights to use the trademarks
58
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
59
+**     of Golden Code Development Corporation. You are not authorized to use the
60
+**     name Golden Code, FWD, or the names of any author or contributor, for
61
+**     publicity purposes without written authorization.
62
+** 
63
+**   2. No Misrepresentation of Affiliation.
64
+** 
65
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
66
+** 
67
+**     You may not represent yourself for publicity purposes as associated with
68
+**     Golden Code Development Corporation, FWD, or any author or contributor to
69
+**     the covered work, without written authorization.
70
+** 
71
+**   3. No Misrepresentation of Source or Origin.
72
+** 
73
+**     You may not represent the covered work as solely your work.  All modified
74
+**     versions of the covered work must be marked in a reasonable way to make it
75
+**     clear that the modified work is not originating from Golden Code Development
76
+**     Corporation or FWD.  All modified versions must contain the notices of
77
+**     attribution required in this license.
78
+*/
79
+
80
+
81
+package com.goldencode.p2j.security;
82
+
83
+
84
+import java.io.*;
85
+import java.net.*;
86
+import java.security.*;
87
+import java.security.cert.*;
88
+import java.util.*;
89
+import java.util.concurrent.TimeUnit;
90
+
91
+import org.bouncycastle.jce.provider.*;
92
+import org.kohsuke.args4j.*;
93
+import org.shredzone.acme4j.*;
94
+import org.shredzone.acme4j.Session;
95
+import org.shredzone.acme4j.Certificate;
96
+import org.shredzone.acme4j.challenge.*;
97
+import org.shredzone.acme4j.exception.*;
98
+import org.shredzone.acme4j.util.*;
99
+import org.slf4j.Logger;
100
+import org.slf4j.LoggerFactory;
101
+
102
+import com.goldencode.p2j.main.ClientsToPortsGenerator;
103
+
104
+
105
+/**
106
+*  Defines the ACME client to get trusted certificates for the target domains. 
107
+*/
108
+public class AcmeClient
109
+{
110
+   /** If the challenge is not accepted for this period, then the client process is stopped. */
111
+   private static final long ACCEPT_CHALLENGE_TIMEOUT = TimeUnit.MINUTES.toMillis(1);
112
+   
113
+   /** The number of tries to get validated challenge */
114
+   private static final int CHALLENGE_TRIES = 100;
115
+   
116
+   /** The polling interval between challenge updates */
117
+   private static final long UPDATE_CHALLENGE_TIMEOUT = 3000;
118
+   
119
+   /** The user private certificate */
120
+   private static final File USER_KEY_FILE = new File("user.key");
121
+   
122
+   /** The user registration URI */
123
+   private static final File USER_REG_FILE = new File("user.reg");
124
+
125
+   /** The target domain private certificate */
126
+   private static final File DOMAIN_KEY_FILE = new File("domain.key");
127
+
128
+   /**  The certificate request for the signed target domain certificate */
129
+   private static final File DOMAIN_CSR_FILE = new File("domain.csr");
130
+
131
+   /** The signed certificate full chain */
132
+   private static final File DOMAIN_CHAIN_FILE = new File("domain-chain.crt");
133
+   
134
+   /** The signed domain certificate */
135
+   private static final File DOMAIN_CRT_FILE = new File("domain.crt");
136
+
137
+   /** The default RSA key size of generated key pairs */
138
+   private static final int KEY_SIZE = 2048;
139
+
140
+   /** The class logger */
141
+   private static final Logger LOG = LoggerFactory.getLogger(AcmeClient.class);
142
+
143
+   /** "acme://letsencrypt.org/staging" */
144
+   private final String acmeServerUri;
145
+   
146
+   /** The managed server host address, the domain IP address */
147
+   private final String host;
148
+   
149
+   /** The managed server port */ 
150
+   private final int port;
151
+   
152
+   /** The instance of the managed web server */
153
+   private ManagedWebServer mws;
154
+   
155
+   /** The current sesion with ACME server */
156
+   private Session session;
157
+   
158
+   /** The ACME client account */
159
+   private Registration registration;
160
+   
161
+   /**
162
+    * Setups the ACME client to use the provided ACME server for its requests and the given
163
+    * ACME accessible host and port.
164
+    * 
165
+    * @param    acmeServerUri
166
+    *           The ACME server URI
167
+    * @param    host
168
+    *           The ACME client host address
169
+    * @param    port
170
+    *           The ACME client port
171
+    */
172
+   public AcmeClient(String acmeServerUri, String host, int port)
173
+   {
174
+      this.acmeServerUri = acmeServerUri;
175
+      this.host          = host;
176
+      this.port          = port;
177
+   }
178
+   
179
+   /**
180
+    * 
181
+    * @param userKeyPair
182
+    * @param contacts
183
+    * @return
184
+    * @throws AcmeException
185
+    */
186
+   public URI registerAccount(KeyPair userKeyPair, List<String> contacts)
187
+   throws AcmeException
188
+   {
189
+      // Create a session for Let's Encrypt.
190
+      // Use "acme://letsencrypt.org" for production server
191
+      // https://acme-staging.api.letsencrypt.org/directory
192
+      session = new Session(acmeServerUri, userKeyPair);
193
+      
194
+      RegistrationBuilder builder = new RegistrationBuilder();
195
+      
196
+      for(String contact : contacts)
197
+      {
198
+         builder.addContact(contact);
199
+      }
200
+      
201
+      try
202
+      {
203
+         registration = builder.create(session);
204
+         
205
+         URI agreement = registration.getAgreement();
206
+         setAgreement(agreement);
207
+      }
208
+      catch (AcmeConflictException ex)
209
+      {
210
+         registration = Registration.bind(session, ex.getLocation());
211
+      }
212
+      
213
+      return registration.getLocation();
214
+   }
215
+   
216
+   /**
217
+    * 
218
+    * @param userKeyPair
219
+    * @param regAccount
220
+    * @throws AcmeException
221
+    */
222
+   public void loginAccount(KeyPair userKeyPair, URI regAccount)
223
+   throws AcmeException
224
+   {
225
+      session = new Session(acmeServerUri, userKeyPair);
226
+      
227
+      registration = Registration.bind(session, regAccount);
228
+   }
229
+   
230
+   /**
231
+    * 
232
+    * @param contact
233
+    * @throws AcmeException
234
+    */
235
+   public void addContact(String contact)
236
+   throws AcmeException
237
+   {
238
+      registration.modify().addContact(contact).commit();
239
+   }
240
+   
241
+   /**
242
+    * Confirms the Terms of Service given by its URI.
243
+    *
244
+    * @param    agreement
245
+    *           The Terms of Service given by its URI
246
+    */
247
+   public void setAgreement(URI agreement)
248
+   throws AcmeException
249
+   {
250
+      registration.modify().setAgreement(agreement).commit();
251
+   }
252
+   
253
+   /**
254
+    * 
255
+    * @param userKeyPair
256
+    * @throws AcmeException
257
+    */
258
+   public void changeUserKey(KeyPair userKeyPair)
259
+   throws AcmeException
260
+   {
261
+      registration.changeKey(userKeyPair);
262
+   }
263
+   
264
+   /**
265
+    * Requests certificates for the given domains.
266
+    * 
267
+    * @param    domains
268
+    *           Domain to get a common certificate for
269
+    */
270
+   public void askCertificate(Map<String, String> certRequestInfo, Collection<String> domains)
271
+   throws IOException, AcmeException
272
+   {
273
+      if (session == null)
274
+      {
275
+         throw new IllegalStateException("The session has not been created yet.");
276
+      }
277
+
278
+      // Separately authorize every requested domain.
279
+      for (String domain : domains)
280
+      {
281
+         authorize(domain);
282
+         try
283
+         {
284
+            mws.shutdown();
285
+         }
286
+         catch (Exception e)
287
+         {
288
+            throw new AcmeException("Can't shutdown the managed server", e);
289
+         }
290
+      }
291
+
292
+      // Load or create a key pair for the domains.
293
+      KeyPair domainKeyPair = loadOrCreateKeyPair(DOMAIN_KEY_FILE);
294
+
295
+      // Generate a CSR for all of the domains, and sign it with the domain key pair.
296
+      CSRBuilder csrb = new CSRBuilder();
297
+      
298
+      String country = certRequestInfo.get("C");
299
+      
300
+      if (country != null)
301
+      {
302
+         csrb.setCountry(country);
303
+      }
304
+      
305
+      String loc = certRequestInfo.get("L");
306
+      
307
+      if (loc != null)
308
+      {
309
+         csrb.setLocality(loc);
310
+      }
311
+      
312
+      String org = certRequestInfo.get("O");
313
+      
314
+      if (org != null)
315
+      {
316
+         csrb.setOrganization(org);
317
+      }
318
+      
319
+      String orgUnit = certRequestInfo.get("OU");
320
+      
321
+      if (orgUnit != null)
322
+      {
323
+         csrb.setOrganizationalUnit(orgUnit);
324
+      }
325
+      
326
+      csrb.addDomains(domains);
327
+      
328
+      csrb.sign(domainKeyPair);
329
+
330
+      try (Writer out = new FileWriter(DOMAIN_CSR_FILE))
331
+      {
332
+         csrb.write(out);
333
+      }
334
+
335
+      Certificate certificate = registration.requestCertificate(csrb.getEncoded());
336
+
337
+      LOG.info("Success! The certificate for domains " + domains + " has been generated!");
338
+      LOG.info("Certificate URI: " + certificate.getLocation());
339
+
340
+      // Download the leaf certificate and certificate chain.
341
+      X509Certificate cert = certificate.download();
342
+      
343
+      // Write the leaf certificate
344
+      try (FileWriter fw = new FileWriter(DOMAIN_CRT_FILE))
345
+      {
346
+         CertificateUtils.writeX509Certificate(cert, fw);
347
+      }
348
+      
349
+      X509Certificate[] chain = certificate.downloadChain();
350
+
351
+      // Write a combined file containing the certificate and chain.
352
+      try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE))
353
+      {
354
+         CertificateUtils.writeX509CertificateChain(fw, cert, chain);
355
+      }
356
+   }
357
+
358
+   /**
359
+    * Loads a key pair from specified file. If the file does not exist,
360
+    * a new key pair is generated and saved.
361
+    *
362
+    * @return {@link KeyPair}.
363
+    */
364
+   private static KeyPair loadOrCreateKeyPair(File file) throws IOException
365
+   {
366
+      if (file.exists())
367
+      {
368
+         try (FileReader fr = new FileReader(file))
369
+         {
370
+            return KeyPairUtils.readKeyPair(fr);
371
+         }
372
+      }
373
+      else
374
+      {
375
+         KeyPair domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
376
+         
377
+         try (FileWriter fw = new FileWriter(file))
378
+         {
379
+            KeyPairUtils.writeKeyPair(domainKeyPair, fw);
380
+         }
381
+         
382
+         return domainKeyPair;
383
+      }
384
+   }
385
+
386
+   /**
387
+    * Serializes user account into the given file.
388
+    * 
389
+    * @param    file
390
+    *           The given file to store the client registration.
391
+    */
392
+   private static void serializeUserAccount(URI regAccount, File file)
393
+   {
394
+      try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)))
395
+      {
396
+         oos.writeObject(regAccount);
397
+         oos.flush();
398
+      }
399
+      catch(IOException ex)
400
+      {
401
+         LOG.info("Can't save the client registration URI '" + regAccount + "'");
402
+      }
403
+   }
404
+   
405
+   /**
406
+    * Reads a user account from the given file.
407
+    * 
408
+    * @param    file
409
+    *           The given file with the client registration.
410
+    */
411
+   private static URI readUserAccount(File file)
412
+   {
413
+      URI regAccount = null;
414
+      
415
+      try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)))
416
+      {
417
+         Object uri = ois.readObject();
418
+         if (!(uri instanceof URI))
419
+         {
420
+            throw new IOException("The file is corrupted");
421
+         }
422
+         
423
+         regAccount = (URI) uri;
424
+      }
425
+      catch(IOException | ClassNotFoundException ex)
426
+      {
427
+         LOG.info("Can't get the client registration URI from " + file.toString() + "'", ex);
428
+      }
429
+      
430
+      return regAccount;
431
+   }
432
+   
433
+   /**
434
+    * Authorize a domain. It will be associated with your account, so you will be able to
435
+    * retrieve a signed certificate for the domain later.
436
+    * <p>
437
+    * You need separate authorizations for subdomains (e.g. "www" subdomain). Wildcard
438
+    * certificates are not currently supported.
439
+    *
440
+    * @param    reg
441
+    *           {@link Registration} of your account
442
+    * @param    domain
443
+    *           Name of the domain to authorize
444
+    * @throws Exception 
445
+    */
446
+   private void authorize(String domain) throws AcmeException
447
+   {
448
+      // Authorize the domain.
449
+      Authorization auth = registration.authorizeDomain(domain);
450
+      LOG.info("Authorization for domain " + domain);
451
+      
452
+      // Find the desired challenge and prepare it.
453
+      Challenge challenge = tlsSniChallenge(auth, domain);
454
+      
455
+      if (challenge == null)
456
+      {
457
+         throw new AcmeException("No challenge found");
458
+      }
459
+
460
+      // If the challenge is already verified, there's no need to execute it again.
461
+      if (challenge.getStatus() == Status.VALID)
462
+      {
463
+         return;
464
+      }
465
+      
466
+      if (!acceptTlsSniChallenge(((TlsSni01Challenge) challenge).getSubject(),
467
+                                 ACCEPT_CHALLENGE_TIMEOUT))
468
+      {
469
+         throw new AcmeException("Challenge is not accepted");
470
+      }
471
+      
472
+      // Now trigger the challenge.
473
+      challenge.trigger();
474
+      
475
+      // Poll for the challenge to complete.
476
+      try
477
+      {
478
+         int attempts = CHALLENGE_TRIES;
479
+         while (challenge.getStatus() != Status.VALID && attempts-- > 0)
480
+         {
481
+            // Did the authorization fail?
482
+            if (challenge.getStatus() == Status.INVALID)
483
+            {
484
+               throw new AcmeException("Challenge failed... Giving up.");
485
+            }
486
+
487
+            // Wait for a few seconds
488
+            Thread.sleep(UPDATE_CHALLENGE_TIMEOUT);
489
+
490
+            // Then update the status
491
+            challenge.update();
492
+         }
493
+      }
494
+      catch (InterruptedException ex)
495
+      {
496
+         LOG.error("interrupted", ex);
497
+         Thread.currentThread().interrupt();
498
+      }
499
+
500
+      // All reattempts are used up and there is still no valid authorization?
501
+      if (challenge.getStatus() != Status.VALID)
502
+      {
503
+         throw new AcmeException("Failed to pass the challenge for domain " + domain
504
+                  + ", ... Giving up.");
505
+      }
506
+   }
507
+
508
+   /**
509
+    * Prepares a TLS-SNI challenge.
510
+    * <p>
511
+    * The verification of this challenge expects that the web server returns a special
512
+    * validation certificate.
513
+    * <p>
514
+    * This example outputs instructions that need to be executed manually. In a
515
+    * production environment, you would rather configure your web server automatically.
516
+    *
517
+    * @param    auth
518
+    *           {@link Authorization} to find the challenge in
519
+    * @param    domain
520
+    *           Domain name to be authorized
521
+    * 
522
+    * @return   {@link Challenge} to verify
523
+    */
524
+   @SuppressWarnings("deprecation")
525
+   // until tls-sni-02 is supported
526
+   public Challenge tlsSniChallenge(Authorization auth, String domain) throws AcmeException
527
+   {
528
+      // Find a single tls-sni-01 challenge
529
+      TlsSni01Challenge challenge = auth.findChallenge(TlsSni01Challenge.TYPE);
530
+      if (challenge == null)
531
+      {
532
+         throw new AcmeException("Found no " + TlsSni01Challenge.TYPE +
533
+                                 " challenge, don't know what to do...");
534
+      }
535
+
536
+      // Get the Subject
537
+      String subject = challenge.getSubject();
538
+
539
+      // Create a validation key pair
540
+      KeyPair domainKeyPair;
541
+      try (FileWriter fw = new FileWriter("tlssni.key"))
542
+      {
543
+         domainKeyPair = KeyPairUtils.createKeyPair(2048);
544
+         KeyPairUtils.writeKeyPair(domainKeyPair, fw);
545
+      }
546
+      catch (IOException ex)
547
+      {
548
+         throw new AcmeException("Could not write keypair", ex);
549
+      }
550
+
551
+      // Create a validation certificate
552
+      try (FileWriter fw = new FileWriter("tlssni.crt"))
553
+      {
554
+         X509Certificate cert = CertificateUtils.createTlsSniCertificate(domainKeyPair, subject);
555
+         CertificateUtils.writeX509Certificate(cert, fw);
556
+      }
557
+      catch (IOException ex)
558
+      {
559
+         throw new AcmeException("Could not write certificate", ex);
560
+      }
561
+
562
+      // Output the challenge, wait for acknowledge...
563
+      LOG.info("Please configure your web server.");
564
+      LOG.info("It must return the certificate 'tlssni.crt' on a SNI request to:");
565
+      LOG.info(subject);
566
+      LOG.info("The matching keypair is available at 'tlssni.key'.");
567
+      LOG.info("If you're ready, dismiss the dialog...");
568
+
569
+      StringBuilder message = new StringBuilder();
570
+      message.append("Please use 'tlssni.key' and 'tlssni.crt' cert for SNI requests to:\n\n");
571
+      message.append("https://").append(subject).append("\n\n");
572
+      LOG.info(message.toString());
573
+      
574
+      return challenge;
575
+   }
576
+
577
+   /**
578
+    * Prepares the challenge validation that starts the managed web server for the provided
579
+    * subject.
580
+    *
581
+    * @param    subject
582
+    *           The provided subject
583
+    * @param    timeout
584
+    *           The waiting time until the server is ready or failed.
585
+    */
586
+   public boolean acceptTlsSniChallenge(String subject, long timeout)
587
+   throws AcmeException
588
+   {
589
+      try
590
+      {
591
+         mws = new ManagedWebServer(host, port, subject);
592
+         
593
+         return mws.waitUntilReady(timeout);
594
+      }
595
+      catch (Exception e)
596
+      {
597
+         throw new AcmeException("Managed web server " + host + ":" + port  + " can't be started",
598
+                                 e);
599
+      }
600
+   }
601
+
602
+   /**
603
+    * Shutdown the managed web server.
604
+    */
605
+   public void shutdown()
606
+   {
607
+      if (mws != null)
608
+      {
609
+         try
610
+         {
611
+            mws.shutdown();
612
+         }
613
+         catch (Exception e)
614
+         {
615
+         }
616
+      }
617
+   }
618
+   
619
+   /**
620
+    * Invokes this example.
621
+    *
622
+    * @param args
623
+    *            Domains to get a certificate for
624
+    */
625
+   public static void main(String... args)
626
+   {
627
+      InputParameters inputParameters = new InputParameters();
628
+      
629
+      CmdLineParser parser = new CmdLineParser(inputParameters);
630
+      
631
+      try
632
+      {
633
+         parser.parseArgument(args);
634
+         
635
+         if (inputParameters.serverUri == null)
636
+         {
637
+            throw new CmdLineException(parser,
638
+                                       "There are no input parameters",
639
+                                       new IllegalArgumentException());
640
+         }
641
+      }
642
+      catch (CmdLineException e)
643
+      {
644
+         System.err.println(e.getMessage());
645
+         parser.printUsage(System.err);
646
+         return;
647
+      }
648
+
649
+      LOG.info("Starting up...");
650
+
651
+      Security.addProvider(new BouncyCastleProvider());
652
+
653
+      Collection<String> domains = Arrays.asList(inputParameters.domains.split(" "));
654
+      
655
+      AcmeClient client = new AcmeClient(inputParameters.serverUri,
656
+                                         inputParameters.host,
657
+                                         inputParameters.port);
658
+      try
659
+      {
660
+         KeyPair userKeyPair = loadOrCreateKeyPair(USER_KEY_FILE);
661
+         
662
+         URI regAccount = readUserAccount(USER_REG_FILE);
663
+         
664
+         if (regAccount == null)
665
+         {
666
+            regAccount = client.registerAccount(userKeyPair, Collections.emptyList());
667
+            serializeUserAccount(regAccount, USER_REG_FILE);
668
+         }
669
+         else
670
+         {
671
+            client.loginAccount(userKeyPair, regAccount);
672
+         }
673
+         
674
+         LOG.info("The client account is " + regAccount);
675
+         
676
+         client.askCertificate(Collections.emptyMap(), domains);
677
+      }
678
+      catch (Exception ex)
679
+      {
680
+         LOG.error("Failed to get a certificate for domains " + domains, ex);
681
+      }
682
+      
683
+      client.shutdown();
684
+   }
685
+
686
+   /**
687
+    * Defines the ACME client input parameters.
688
+    */
689
+   static class InputParameters
690
+   {
691
+      /**
692
+       * The domain host address. Can be set via "-host" option.
693
+       */
694
+      @Option(name="-host",
695
+              usage="The domain host address.")
696
+      public String host;
697
+
698
+      /**
699
+       * The internal redirected port or 443 standard HTTPS port. Can be set via "-port" option.
700
+       * It must be 443 only or the redirected port number. ACME client must be available
701
+       * for external ACME servers via the requested domain name and the port number 443.
702
+       */
703
+      @Option(name="-port",
704
+              usage="The domain port.")
705
+      public Integer port;
706
+
707
+      /**
708
+       * The ACME server URI. Can be set via "-server" option. For the testing purpose it must be
709
+       * "acme://letsencrypt.org/staging".
710
+       */
711
+      @Option(name="-server",
712
+              depends={"-host","-port", "-domains"},
713
+              usage="The ACME server URI.")
714
+      public String serverUri;
715
+      
716
+      /**
717
+       * The list of requested domains separated by spaces that is similar to this example,
718
+       * "test1.acme.com test2.acme.com test3.acme.com". The wild cards are not supported. 
719
+       */
720
+      @Option(name="-domains",
721
+              usage="The requested domains enclosed in one string within double quoters and " +
722
+                    "separated by spaces.")
723
+      public String domains;
724
+   }
725
+
726
+}
727

    
728
=== added file 'src/com/goldencode/p2j/security/ManagedWebServer.java'
729
--- src/com/goldencode/p2j/security/ManagedWebServer.java	1970-01-01 00:00:00 +0000
730
+++ src/com/goldencode/p2j/security/ManagedWebServer.java	2017-10-31 08:47:43 +0000
731
@@ -0,0 +1,373 @@
732
+/*
733
+** Module   : ManagedWebServer.java
734
+** Abstract : Implements Managed Web Server to prove the ownership of the target domain.
735
+**
736
+** Copyright (c) 2017, Golden Code Development Corporation.
737
+**
738
+** -#- -I- --Date-- ---------------------------------Description----------------------------------
739
+** 001 SBI 20171030 First version.
740
+*/
741
+/*
742
+** This program is free software: you can redistribute it and/or modify
743
+** it under the terms of the GNU Affero General Public License as
744
+** published by the Free Software Foundation, either version 3 of the
745
+** License, or (at your option) any later version.
746
+**
747
+** This program is distributed in the hope that it will be useful,
748
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
749
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
750
+** GNU Affero General Public License for more details.
751
+**
752
+** You may find a copy of the GNU Affero GPL version 3 at the following
753
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
754
+** 
755
+** Additional terms under GNU Affero GPL version 3 section 7:
756
+** 
757
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
758
+**   terms apply to the works covered under the License.  These additional terms
759
+**   are non-permissive additional terms allowed under Section 7 of the GNU
760
+**   Affero GPL version 3 and may not be removed by you.
761
+** 
762
+**   0. Attribution Requirement.
763
+** 
764
+**     You must preserve all legal notices or author attributions in the covered
765
+**     work or Appropriate Legal Notices displayed by works containing the covered
766
+**     work.  You may not remove from the covered work any author or developer
767
+**     credit already included within the covered work.
768
+** 
769
+**   1. No License To Use Trademarks.
770
+** 
771
+**     This license does not grant any license or rights to use the trademarks
772
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
773
+**     of Golden Code Development Corporation. You are not authorized to use the
774
+**     name Golden Code, FWD, or the names of any author or contributor, for
775
+**     publicity purposes without written authorization.
776
+** 
777
+**   2. No Misrepresentation of Affiliation.
778
+** 
779
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
780
+** 
781
+**     You may not represent yourself for publicity purposes as associated with
782
+**     Golden Code Development Corporation, FWD, or any author or contributor to
783
+**     the covered work, without written authorization.
784
+** 
785
+**   3. No Misrepresentation of Source or Origin.
786
+** 
787
+**     You may not represent the covered work as solely your work.  All modified
788
+**     versions of the covered work must be marked in a reasonable way to make it
789
+**     clear that the modified work is not originating from Golden Code Development
790
+**     Corporation or FWD.  All modified versions must contain the notices of
791
+**     attribution required in this license.
792
+*/
793
+
794
+
795
+package com.goldencode.p2j.security;
796
+
797
+import java.io.*;
798
+import java.security.*;
799
+import java.security.cert.*;
800
+import java.security.cert.Certificate;
801
+import java.util.concurrent.*;
802
+
803
+import org.eclipse.jetty.client.*;
804
+import org.eclipse.jetty.client.api.*;
805
+import org.eclipse.jetty.client.api.Response.CompleteListener;
806
+import org.eclipse.jetty.http.*;
807
+import org.eclipse.jetty.server.handler.*;
808
+import org.eclipse.jetty.util.ssl.*;
809
+import org.kohsuke.args4j.*;
810
+
811
+import com.goldencode.p2j.web.*;
812
+import com.goldencode.util.*;
813
+
814
+
815
+
816
+/**
817
+ * Represents the functionality to be able to start an instance of the managed web server that
818
+ * responds on SNI requests to the subject provided by the ACME server in order to prove
819
+ * the ownership of this host.
820
+ */
821
+public class ManagedWebServer
822
+{
823
+   /** The request timeout */
824
+   private static final long REQUEST_TIMEOUT = 1000;
825
+   
826
+   /** The file name of the server private certificate */
827
+   private static final String SERVER_KEY_FILE = "tlssni.key";
828
+
829
+   /** The file name of the server public certificate */
830
+   private static final String SERVER_CRT_FILE = "tlssni.crt";
831
+   
832
+   /** The server alias */
833
+   private static final String SERVER_ALIAS = "managed-server";
834
+   
835
+   /** The server key store */
836
+   private static final String SERVER_STORE = "managed-server.store";
837
+   
838
+   /** The secure web server */
839
+   private final SecureWebServer server;
840
+   
841
+   /** The private key store */
842
+   private final KeyStore keyStore;
843
+   
844
+   /** The public key store */
845
+   private final KeyStore certStore;
846
+   
847
+   /** The certificate factory */
848
+   private SSLCertFactory factory;
849
+   
850
+   /** The http client */
851
+   private HttpClient client;
852
+   
853
+   /** Indicates if the server is ready to respond on SNI requests to the subject. */
854
+   private boolean isReady;
855
+   
856
+   /**
857
+    * Starts an instance of the managed https web server that responds on SNI requests to the
858
+    * subject provided by the ACME server in order to prove the ownership of this host.
859
+    * 
860
+    * @param    host
861
+    *           The host address
862
+    * @param    port
863
+    *           The available host port
864
+    * @param    subject
865
+    *           The provided virtual host name by ACME server in order to prove the ownership of
866
+    *           this host. It can be written to follow this host name pattern
867
+    *           "f7ea254ebdbfc4a41637cb08294f3721.772069a9f4ff4bf7cc88bc40ba9f01b2.acme.invalid".
868
+    * 
869
+    * @throws   Exception
870
+    *           Iff at least one of the web server and its client is failed to start.
871
+    */
872
+   public ManagedWebServer(String host, int port, String subject) throws Exception
873
+   {
874
+      factory = new BCCertFactory();
875
+      
876
+      keyStore = SSLCertGenUtil.createEmptyStore();
877
+      
878
+      certStore = SSLCertGenUtil.createEmptyStore();
879
+      
880
+      factory.loadRootCA(SERVER_CRT_FILE, SERVER_KEY_FILE, "");
881
+      
882
+      // key entry password
883
+      String keyEntryPassword = factory.saveRootCA(SERVER_ALIAS, certStore, keyStore);
884
+      
885
+      try
886
+      {
887
+         Key rootKey = keyStore.getKey(SERVER_ALIAS, keyEntryPassword.toCharArray());
888
+         Certificate rootCert = certStore.getCertificate(SERVER_ALIAS);
889
+         
890
+         KeyStore store = SSLCertGenUtil.createEmptyStore();
891
+         store.setKeyEntry(SERVER_ALIAS, rootKey, keyEntryPassword.toCharArray(),
892
+                           new Certificate[] { rootCert });
893
+         
894
+         String keyStorePassword = RandomWordGenerator.create();
895
+         store.store(new FileOutputStream(SERVER_STORE), keyStorePassword.toCharArray());
896
+         
897
+         server = new SecureWebServer(SERVER_STORE, keyStorePassword, keyEntryPassword);
898
+      }
899
+      catch (UnrecoverableKeyException |
900
+             KeyStoreException |
901
+             NoSuchAlgorithmException |
902
+             CertificateException |
903
+             IOException e)
904
+      {
905
+         throw new SSLCertGenException(e);
906
+      }
907
+
908
+      ResourceHandler rootHandler = new ResourceHandler();
909
+      rootHandler.setDirectoriesListed(false);
910
+      
911
+      ContextHandler rootContext = new ContextHandler();
912
+      rootContext.setContextPath("/");
913
+      rootContext.setHandler(rootHandler);
914
+
915
+      rootContext.addVirtualHosts(
916
+               new String[] {subject});
917
+      
918
+      ContextHandler handler = new ContextHandler("/");
919
+      handler.setHandler(rootContext);
920
+      server.addHandler(handler);
921
+      server.startup(host, port);
922
+      
923
+      SslContextFactory sslContextFactory = new SslContextFactory();
924
+      
925
+      sslContextFactory.setTrustStore(certStore);
926
+      
927
+      client = new HttpClient(sslContextFactory);
928
+      client.start();
929
+   }
930
+
931
+   /**
932
+    * Waits if the server is ready or the elapsed time exceeds the given timeout until the first
933
+    * event occurs.
934
+    *  
935
+    * @param    timeout
936
+    *           The given timeout
937
+    * 
938
+    * @return   True iff the server is ready.
939
+    */
940
+   public boolean waitUntilReady(long timeout)
941
+   {
942
+      isReady = false;
943
+      
944
+      CountDownLatch ready = new CountDownLatch(1);
945
+      
946
+      CompleteListener callback = new CompleteListener()
947
+      {
948
+         
949
+         @Override
950
+         public void onComplete(Result result)
951
+         {
952
+            isReady = !result.isFailed();
953
+            if (isReady)
954
+            {
955
+               ready.countDown();
956
+            }
957
+         }
958
+      };
959
+      
960
+      long startWaiting = System.currentTimeMillis();
961
+      
962
+      long elapsedTime = 0;
963
+      
964
+      while(!isReady && ready.getCount() == 1)
965
+      {
966
+         isServerReady(callback, REQUEST_TIMEOUT);
967
+         try
968
+         {
969
+            ready.await(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS);
970
+         }
971
+         catch (InterruptedException e)
972
+         {
973
+         }
974
+         finally
975
+         {
976
+            elapsedTime = System.currentTimeMillis() - startWaiting;
977
+         }
978
+         
979
+         if (elapsedTime >= timeout)
980
+         {
981
+            ready.countDown();
982
+         }
983
+      }
984
+      
985
+      return isReady;
986
+   }
987
+   
988
+   /**
989
+    * Tests asynchronously if the target server is ready.
990
+    *  
991
+    * @param    callback
992
+    *           The given callback
993
+    * @param    timeout
994
+    *           The https request timeout
995
+    */
996
+   private void isServerReady(CompleteListener callback, long timeout)
997
+   {
998
+      StringBuilder connectTest = new StringBuilder();
999
+      connectTest.append(HttpScheme.HTTPS.asString())
1000
+                 .append("://")
1001
+                 .append(server.getHost()).append(":")
1002
+                 .append(server.getPort()).append("/");
1003
+      client.newRequest(connectTest.toString())
1004
+            .timeout(timeout, TimeUnit.MILLISECONDS)
1005
+            .send(callback);
1006
+   }
1007
+   
1008
+   /**
1009
+    * Stops the web server and its client.
1010
+    * 
1011
+    * @throws   Exception
1012
+    *           iff the shutdown operation is failed
1013
+    */
1014
+   public void shutdown()
1015
+   throws Exception
1016
+   {
1017
+      server.shutdown();
1018
+      server.join();
1019
+      client.stop();
1020
+   }
1021
+   
1022
+   /**
1023
+    * Starts the managed web server from the given command line arguments following this pattern:
1024
+    * -host "127.0.0.1" -port 8888 -subject "test.acme.invalid".
1025
+    * 
1026
+    * @param    args
1027
+    *           The provided parameters
1028
+    */
1029
+   public static void main(String[] args)
1030
+   {
1031
+      InputParameters inputParameters = new InputParameters();
1032
+      
1033
+      CmdLineParser parser = new CmdLineParser(inputParameters);
1034
+      
1035
+      try
1036
+      {
1037
+         parser.parseArgument(args);
1038
+      }
1039
+      catch (CmdLineException e)
1040
+      {
1041
+         System.err.println(e.getMessage());
1042
+         parser.printUsage(System.err);
1043
+         return;
1044
+      }
1045
+      
1046
+      try
1047
+      {
1048
+         
1049
+         ManagedWebServer mws = new ManagedWebServer(inputParameters.host,
1050
+                                                     inputParameters.port,
1051
+                                                     inputParameters.subject);
1052
+         
1053
+         boolean testResult = mws.waitUntilReady(10000);
1054
+         
1055
+         String msg;
1056
+         
1057
+         if (testResult)
1058
+         {
1059
+            msg = "The server is started successfully.";
1060
+         }
1061
+         else
1062
+         {
1063
+            msg = "The server is failed to start.";
1064
+         }
1065
+         
1066
+         System.out.println(msg);
1067
+         
1068
+         if (!testResult)
1069
+         {
1070
+            System.exit(-1);
1071
+         }
1072
+      }
1073
+      catch (Exception e)
1074
+      {
1075
+         e.printStackTrace();
1076
+      }
1077
+   }
1078
+   
1079
+   /**
1080
+    * Defines the managed web server input parameters.
1081
+    */
1082
+   static class InputParameters
1083
+   {
1084
+      /**
1085
+       * The server host address. Can be set via "-host" option.
1086
+       */
1087
+      @Option(name="-host",
1088
+              usage="The server host address.")
1089
+      public String host;
1090
+
1091
+      /**
1092
+       * The server port. Can be set via "-port" option.
1093
+       */
1094
+      @Option(name="-port", usage="The server port.")
1095
+      public Integer port;
1096
+
1097
+      /**
1098
+       * The provided subject. Can be set via "-subject" option.
1099
+       */
1100
+      @Option(name="-subject",
1101
+              usage="The provided subject.")
1102
+      public String subject;
1103
+   }
1104
+}
1105

    
1106
=== modified file 'src/com/goldencode/p2j/security/SSLCertGenUtil.java'
1107
--- src/com/goldencode/p2j/security/SSLCertGenUtil.java	2017-10-30 14:18:34 +0000
1108
+++ src/com/goldencode/p2j/security/SSLCertGenUtil.java	2017-10-30 23:47:56 +0000
1109
@@ -1618,7 +1618,7 @@
1110
     * @throws   SSLCertGenException
1111
     *           If the store could not be generated.
1112
     */
1113
-   private KeyStore createEmptyStore()
1114
+   static KeyStore createEmptyStore()
1115
    throws SSLCertGenException
1116
    {
1117
       try
1118