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
|
|