如何生成测试用服务端和客户端证书

由于项目开发需要实现服务端和客户端的SSL双向认证,需要用到测试数据验证,下面介绍生成证书的步骤。

生成CA根证书

  1. 生成一个CA的私钥
1
2
#generate ca key
openssl genpkey -algorithm RSA -out ca.key
  1. 根据CA的私钥,生成一份自签名的CA根证书
1
2
3
# generate self-signed ca certificate
openssl req -new -x509 -days 365 -key ca.key -out ca.pem \
-subj "/C=cn/O=mycomp/OU=myorg/CN=ca/emailAddress=ca@example.com"

生成服务端证书

  1. 生成服务端私钥
1
2
# generate server key
openssl genpkey -algorithm RSA -out server.key
  1. 生成服务端证书CSR
1
2
3
#generate server certificate signing request
openssl req -new -nodes -days 365 -key server.key -out server.csr \
-subj "/C=cn/O=mycomp/OU=myorg/CN=server/emailAddress=server@example.com"
  1. 使用前面生成的CA根证书对服务端的CSR签名,生成服务端证书
1
2
3
4
# generate server certificate
openssl x509 -req -days 365 \
-in server.csr -out server.pem \
-CA ca.pem -CAkey ca.key -CAcreateserial

生成客户端证书

  1. 生成客户端私钥
1
2
# generate client key
openssl genpkey -algorithm RSA -out client.key
  1. 生成客户端证书CSR
1
2
3
# generate client certificate signing request
openssl req -new -nodes -days 365 -key client.key -out client.csr \
-subj "/C=cn/O=mycomp/OU=myorg/CN=client/emailAddress=client@example.com"
  1. 使用前面生成的CA根证书对客户端的CSR签名,生成客户端证书
1
2
3
# generate client certificate
openssl x509 -req -days 365 -in client.csr -out client.pem \
-CA ca.pem -CAkey ca.key -CAcreateserial

检查生成的服务端和客户端证书

1
2
openssl x509 -noout -text -in server.pem
openssl x509 -noout -text -in client.pem

生成trustore

因为服务端和客户端使用同一个CA签发的证书,所以在此服务端和客户端使用同一个truststore就可以了,即truststore只包含一个此CA证书信息。

1
2
3
# generate server/client truststore
keytool -import -noprompt -file ca.pem \
-keystore truststore.jks -storepass test1234

生成服务端keystore

  1. 生成服务端的P12格式证书
1
2
3
4
# generate server p12
openssl pkcs12 -export \
-in server.pem -inkey server.key \
-out server.p12 -passout pass:test1234
  1. 生成服务端的JKS格式证书
1
2
3
4
5
6
# generate server jks
keytool -importkeystore \
-srckeystore server.p12 -srcstoretype PKCS12 \
-destkeystore server.keystore.jks \
-srcstorepass test1234 \
-deststorepass test1234

生成客户端keystore

  1. 生成客户端的P12格式证书
1
2
3
4
# generate client p12
openssl pkcs12 -export \
-in client.pem -inkey client.key -out client.p12 \
-passout pass:test1234
  1. 生成客户端的JKS格式证书
1
2
3
4
5
6
# generate client jks
keytool -importkeystore \
-srckeystore client.p12 -srcstoretype PKCS12 \
-destkeystore client.keystore.jks \
-srcstorepass test1234 \
-deststorepass test1234

验证JKS证书的内容

1
2
3
keytool --list -keystore client.keystore.jks -storepass test1234
keytool --list -keystore server.keystore.jks -storepass testr1234
keytool --list -keystore truststore.jks -storepass test1234

更改访问主机域名

前面步骤中,生成的服务端和客户端的证书里面配置了证书subject的CN域为severclient,在SSL验证主机名的时候要求主机的名字必须是serverclient,否则SSL验证会报失败。

这个结果就是这个证书必须在主机名为server/client的机器上使用,不能在其它机器上使用。这个限制可不好,所以SSL标准定义了证书扩展属性SAN(Subject Alternative Name),这是SSL 标准 x509 中定义的一个扩展,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。

具体怎么操作:

  1. 首先配置openssl.cnf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[ CA_default ]
...
# Extension copying option: use with caution.
copy_extensions = copy

[ req ]
...
req_extensions = v3_req # The extensions to add to a certificate request

[ v3_req ]
...
# Extensions to add to a certificate request
...
subjectAltName = @alt_names

[alt_names]
DNS.1 = *.kafka.example.com
DNS.2 = *.example.com
  1. 把前面步骤修改成如下,即指定扩展属性的openssl.cnf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# step 2.2: generate server certificate signing request
openssl req -new -nodes -days 365 -key server.key -out server.csr \
-subj "/C=cn/O=mycomp/OU=myorg/CN=server/emailAddress=server@example.com" \
-config ./openssl.cnf -extensions v3_req

# step 2.3: generate server certificate
openssl x509 -req -days 365 \
-in server.csr -out server.pem \
-CA ca.pem -CAkey ca.key -CAcreateserial \
-extfile ./openssl.cnf -extensions v3_req

# step 3.2: generate client certificate signing request
openssl req -new -nodes -days 365 -key client.key -out client.csr 、
-subj "/C=CN/O=mycomp/OU=myorg/CN=client/emailAddress=client@example.com" \
-config ./openssl.cnf -extensions v3_req

# step 3.3: generate client certificate
openssl x509 -req -days 365 -in client.csr -out client.pem \
-CA ca.pem -CAkey ca.key -CAcreateserial \
-extfile ./openssl.cnf -extensions v3_req
  1. 如果不想修改openssl.cnf,也可以直接使用命令行的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# step 2.2: generate server certificate signing request
openssl req -new -nodes -days 365 -key server.key -out server.csr \
-subj "/C=cn/O=mycomp/OU=myorg/CN=server/emailAddress=server@example.com" \
-reqexts SAN \
-config <(cat /etc/pki/tls/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.kafka.example.com,DNS:*.example.com"))

# step 2.3: generate server certificate
openssl x509 -req -days 365 \
-in server.csr -out server.pem \
-CA ca.pem -CAkey ca.key -CAcreateserial \
-extensions SAN \
-extfile <(cat /etc/pki/tls/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:*.kafka.example.com,DNS:*.example.com"))

# step 3.2: generate client certificate signing request
openssl req -new -nodes -days 365 -key client.key -out client.csr 、
-subj "/C=CN/O=mycomp/OU=myorg/CN=client/emailAddress=client@example.com" \
-reqexts SAN \
-config <(cat /etc/pki/tls/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.kafka.example.com,DNS:*.example.com"))

# step 3.3: generate client certificate
openssl x509 -req -days 365 -in client.csr -out client.pem \
-CA ca.pem -CAkey ca.key -CAcreateserial \
-extensions SAN \
-extfile <(cat /etc/pki/tls/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:*.kafka.example.com,DNS:*.example.com"))

椭圆算法(EC)的证书签名

标准上除了定义RSA的签名算法,还有ECDSA(又叫EC)、DSA、PKCS8和PKCS8加密这几种。

1
2
3
4
5
6
7
8
9
10
openssl ecparam  -name secp256k1 -genkey

-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIKYV1xoz6smkpdMksfgI8/3465V02UZdaKj4JSH30bBhoAcGBSuBBAAK
oUQDQgAEO1O+/xRGEVJgBEAOQorBveXPTQS3c7MA+9R+HEMP7TkscI9FONPclcRb
5sXZJjYHNYWhvxuXdGl8QrFVRIVBYg==
-----END EC PRIVATE KEY-----

ecparam是一个椭圆参数,用于实现私钥的生成,它是一个可选性。

如何在客户端发起SSL的请求

假设实现两套SSL连接,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

/**
* SSLContext factory for handle client connection
*
*/
public class SSLContextFactory {

// pem证书方式,带有证书和私钥
public static SSLContext generate(
Collection<Path> certPaths,
Collection<Path> caPaths,
Path keyPath,
String keyStorePass)
throws GeneralSecurityException, IOException {
List<Certificate> certificate = PemUtils.readCertificates(certPaths);
PrivateKey privateKey = PemUtils.readPrivateKey(keyPath, keyStorePass::toCharArray);
List<Certificate> chain = PemUtils.readCertificates(caPaths);
KeyStore keyStore =
KeyStoreUtil.buildKeyStore(certificate, privateKey, keyStorePass.toCharArray());
KeyStore trustKeySore = KeyStoreUtil.buildTrustStore(chain);
return SSLContexts.custom()
.loadKeyMaterial(keyStore, keyStorePass.toCharArray())
.loadTrustMaterial(trustKeySore, null)
.build();
}
// keystore 方式,用生成的JKS文件(可能是jks、p12、pfx、pkcs12)发起SSLContext
public static SSLContext generate(Path keyStorePath, String keyStorePass, String keyStoreType)
throws GeneralSecurityException, IOException {
KeyStore trustStore =
KeyStoreUtil.buildDefaultStore(keyStorePath, keyStorePass::toCharArray, keyStoreType);
return SSLContexts.custom()
.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy())
.build();
}
}
  1. 首先是对不同的算法的私钥进行检测,解析得出PrivateKey对象;其次是解析证书为Certificate对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625

final class PemUtils {

private static final String PKCS1_HEADER = "-----BEGIN RSA PRIVATE KEY-----";
private static final String PKCS1_FOOTER = "-----END RSA PRIVATE KEY-----";
private static final String OPENSSL_DSA_HEADER = "-----BEGIN DSA PRIVATE KEY-----";
private static final String OPENSSL_DSA_FOOTER = "-----END DSA PRIVATE KEY-----";
private static final String OPENSSL_DSA_PARAMS_HEADER = "-----BEGIN DSA PARAMETERS-----";
private static final String OPENSSL_DSA_PARAMS_FOOTER = "-----END DSA PARAMETERS-----";
private static final String PKCS8_HEADER = "-----BEGIN PRIVATE KEY-----";
private static final String PKCS8_FOOTER = "-----END PRIVATE KEY-----";
private static final String PKCS8_ENCRYPTED_HEADER = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
private static final String PKCS8_ENCRYPTED_FOOTER = "-----END ENCRYPTED PRIVATE KEY-----";
private static final String OPENSSL_EC_HEADER = "-----BEGIN EC PRIVATE KEY-----";
private static final String OPENSSL_EC_FOOTER = "-----END EC PRIVATE KEY-----";
private static final String OPENSSL_EC_PARAMS_HEADER = "-----BEGIN EC PARAMETERS-----";
private static final String OPENSSL_EC_PARAMS_FOOTER = "-----END EC PARAMETERS-----";
private static final String HEADER = "-----BEGIN";

private PemUtils() {
throw new IllegalStateException("Utility class should not be instantiated");
}

/**
* Creates a {@link PrivateKey} from the contents of a file. Supports PKCS#1, PKCS#8 encoded
* formats of encrypted and plaintext RSA, DSA and EC(secp256r1) keys
*
* @param keyPath the path for the key file
* @param passwordSupplier A password supplier for the potentially encrypted (password
* protected) key
* @return a private key from the contents of the file
*/
static PrivateKey readPrivateKey(Path keyPath, Supplier<char[]> passwordSupplier) {
try (BufferedReader bReader = Files.newBufferedReader(keyPath, StandardCharsets.UTF_8)) {
String line = bReader.readLine();
while (null != line && !line.startsWith(HEADER)) {
line = bReader.readLine();
}
if (null == line) {
throw new SslConfigException(
"Error parsing Private Key ["
+ keyPath.toAbsolutePath()
+ "], file is empty");
}
switch (line.trim()) {
case PKCS8_ENCRYPTED_HEADER:
char[] password = passwordSupplier.get();
if (password == null) {
throw new SslConfigException(
"cannot read encrypted key ["
+ keyPath.toAbsolutePath()
+ "] "
+ "without a password");
}
return parsePKCS8Encrypted(bReader, password);
case PKCS8_HEADER:
return parsePKCS8(bReader);
case PKCS1_HEADER:
return parsePKCS1Rsa(bReader, passwordSupplier);
case OPENSSL_DSA_HEADER:
return parseOpenSslDsa(bReader, passwordSupplier);
case OPENSSL_DSA_PARAMS_HEADER:
return parseOpenSslDsa(removeDsaHeaders(bReader), passwordSupplier);
case OPENSSL_EC_HEADER:
return parseOpenSslEC(bReader, passwordSupplier);
case OPENSSL_EC_PARAMS_HEADER:
return parseOpenSslEC(removeECHeaders(bReader), passwordSupplier);
default:
throw new SslConfigException(
"error parsing Private Key ["
+ keyPath.toAbsolutePath()
+ "], file "
+ "does not contain a supported key format");
}
} catch (FileNotFoundException | NoSuchFileException e) {
throw new SslConfigException(
"private key file [" + keyPath.toAbsolutePath() + "] does not exist", e);
} catch (IOException | GeneralSecurityException e) {
throw new SslConfigException(
"private key file [" + keyPath.toAbsolutePath() + "] cannot be parsed", e);
}
}

/**
* Removes the EC Headers that OpenSSL adds to EC private keys as the information in them is
* redundant
*
* @throws IOException if the EC Parameter footer is missing
*/
private static BufferedReader removeECHeaders(BufferedReader bReader) throws IOException {
String line = bReader.readLine();
while (line != null) {
if (OPENSSL_EC_PARAMS_FOOTER.equals(line.trim())) {
break;
}
line = bReader.readLine();
}
if (null == line) {
throw new IOException("Malformed PEM file, EC Parameters footer is missing");
}
// Verify that the key starts with the correct header before passing it to parseOpenSslEC
if (!OPENSSL_EC_HEADER.equals(bReader.readLine())) {
throw new IOException("Malformed PEM file, EC Key header is missing");
}
return bReader;
}

/**
* Removes the DSA Params Headers that OpenSSL adds to DSA private keys as the information in
* them is redundant
*
* @throws IOException if the EC Parameter footer is missing
*/
private static BufferedReader removeDsaHeaders(BufferedReader bReader) throws IOException {
String line = bReader.readLine();
while (line != null) {
if (OPENSSL_DSA_PARAMS_FOOTER.equals(line.trim())) {
break;
}
line = bReader.readLine();
}
if (null == line) {
throw new IOException("Malformed PEM file, DSA Parameters footer is missing");
}
// Verify that the key starts with the correct header before passing it to parseOpenSslDsa
if (!OPENSSL_DSA_HEADER.equals(bReader.readLine())) {
throw new IOException("Malformed PEM file, DSA Key header is missing");
}
return bReader;
}

/**
* Creates a {@link PrivateKey} from the contents of {@code bReader} that contains an plaintext
* private key encoded in PKCS#8
*
* @param bReader the {@link BufferedReader} containing the key file contents
* @return {@link PrivateKey}
* @throws IOException if the file can't be read
* @throws GeneralSecurityException if the private key can't be generated from the {@link
* PKCS8EncodedKeySpec}
*/
private static PrivateKey parsePKCS8(BufferedReader bReader)
throws IOException, GeneralSecurityException {
StringBuilder sb = new StringBuilder();
String line = bReader.readLine();
while (line != null) {
if (PKCS8_FOOTER.equals(line.trim())) {
break;
}
sb.append(line.trim());
line = bReader.readLine();
}
if (null == line) {
throw new IOException("Malformed PEM file, PEM footer is invalid or missing");
}
byte[] keyBytes = Base64.getDecoder().decode(sb.toString());
String keyAlgo = getKeyAlgorithmIdentifier(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(keyAlgo);
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
}

/**
* Creates a {@link PrivateKey} from the contents of {@code bReader} that contains an EC private
* key encoded in OpenSSL traditional format.
*
* @param bReader the {@link BufferedReader} containing the key file contents
* @param passwordSupplier A password supplier for the potentially encrypted (password
* protected) key
* @return {@link PrivateKey}
* @throws IOException if the file can't be read
* @throws GeneralSecurityException if the private key can't be generated from the {@link
* ECPrivateKeySpec}
*/
private static PrivateKey parseOpenSslEC(
BufferedReader bReader, Supplier<char[]> passwordSupplier)
throws IOException, GeneralSecurityException {
StringBuilder sb = new StringBuilder();
String line = bReader.readLine();
Map<String, String> pemHeaders = new HashMap<>();
while (line != null) {
if (OPENSSL_EC_FOOTER.equals(line.trim())) {
break;
}
// Parse PEM headers according to https://www.ietf.org/rfc/rfc1421.txt
if (line.contains(":")) {
String[] header = line.split(":");
pemHeaders.put(header[0].trim(), header[1].trim());
} else {
sb.append(line.trim());
}
line = bReader.readLine();
}
if (null == line) {
throw new IOException("Malformed PEM file, PEM footer is invalid or missing");
}
byte[] keyBytes = possiblyDecryptPKCS1Key(pemHeaders, sb.toString(), passwordSupplier);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
ECPrivateKeySpec ecSpec = parseEcDer(keyBytes);
return keyFactory.generatePrivate(ecSpec);
}

/**
* Creates a {@link PrivateKey} from the contents of {@code bReader} that contains an RSA
* private key encoded in OpenSSL traditional format.
*
* @param bReader the {@link BufferedReader} containing the key file contents
* @param passwordSupplier A password supplier for the potentially encrypted (password
* protected) key
* @return {@link PrivateKey}
* @throws IOException if the file can't be read
* @throws GeneralSecurityException if the private key can't be generated from the {@link
* RSAPrivateCrtKeySpec}
*/
private static PrivateKey parsePKCS1Rsa(
BufferedReader bReader, Supplier<char[]> passwordSupplier)
throws IOException, GeneralSecurityException {
StringBuilder sb = new StringBuilder();
String line = bReader.readLine();
Map<String, String> pemHeaders = new HashMap<>();

while (line != null) {
if (PKCS1_FOOTER.equals(line.trim())) {
// Unencrypted
break;
}
// Parse PEM headers according to https://www.ietf.org/rfc/rfc1421.txt
if (line.contains(":")) {
String[] header = line.split(":");
pemHeaders.put(header[0].trim(), header[1].trim());
} else {
sb.append(line.trim());
}
line = bReader.readLine();
}
if (null == line) {
throw new IOException("Malformed PEM file, PEM footer is invalid or missing");
}
byte[] keyBytes = possiblyDecryptPKCS1Key(pemHeaders, sb.toString(), passwordSupplier);
RSAPrivateCrtKeySpec spec = parseRsaDer(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(spec);
}

/**
* Creates a {@link PrivateKey} from the contents of {@code bReader} that contains an DSA
* private key encoded in OpenSSL traditional format.
*
* @param bReader the {@link BufferedReader} containing the key file contents
* @param passwordSupplier A password supplier for the potentially encrypted (password
* protected) key
* @return {@link PrivateKey}
* @throws IOException if the file can't be read
* @throws GeneralSecurityException if the private key can't be generated from the {@link
* DSAPrivateKeySpec}
*/
private static PrivateKey parseOpenSslDsa(
BufferedReader bReader, Supplier<char[]> passwordSupplier)
throws IOException, GeneralSecurityException {
StringBuilder sb = new StringBuilder();
String line = bReader.readLine();
Map<String, String> pemHeaders = new HashMap<>();

while (line != null) {
if (OPENSSL_DSA_FOOTER.equals(line.trim())) {
// Unencrypted
break;
}
// Parse PEM headers according to https://www.ietf.org/rfc/rfc1421.txt
if (line.contains(":")) {
String[] header = line.split(":");
pemHeaders.put(header[0].trim(), header[1].trim());
} else {
sb.append(line.trim());
}
line = bReader.readLine();
}
if (null == line) {
throw new IOException("Malformed PEM file, PEM footer is invalid or missing");
}
byte[] keyBytes = possiblyDecryptPKCS1Key(pemHeaders, sb.toString(), passwordSupplier);
DSAPrivateKeySpec spec = parseDsaDer(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
return keyFactory.generatePrivate(spec);
}

/**
* Creates a {@link PrivateKey} from the contents of {@code bReader} that contains an encrypted
* private key encoded in PKCS#8
*
* @param bReader the {@link BufferedReader} containing the key file contents
* @param keyPassword The password for the encrypted (password protected) key
* @return {@link PrivateKey}
* @throws IOException if the file can't be read
* @throws GeneralSecurityException if the private key can't be generated from the {@link
* PKCS8EncodedKeySpec}
*/
private static PrivateKey parsePKCS8Encrypted(BufferedReader bReader, char[] keyPassword)
throws IOException, GeneralSecurityException {
StringBuilder sb = new StringBuilder();
String line = bReader.readLine();
while (line != null) {
if (PKCS8_ENCRYPTED_FOOTER.equals(line.trim())) {
break;
}
sb.append(line.trim());
line = bReader.readLine();
}
if (null == line) {
throw new IOException("Malformed PEM file, PEM footer is invalid or missing");
}
byte[] keyBytes = Base64.getDecoder().decode(sb.toString());

EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(keyBytes);
SecretKeyFactory secretKeyFactory =
SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName());
SecretKey secretKey = secretKeyFactory.generateSecret(new PBEKeySpec(keyPassword));
Arrays.fill(keyPassword, '\u0000');
Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName());
cipher.init(Cipher.DECRYPT_MODE, secretKey, encryptedPrivateKeyInfo.getAlgParameters());
PKCS8EncodedKeySpec keySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
String keyAlgo = getKeyAlgorithmIdentifier(keySpec.getEncoded());
KeyFactory keyFactory = KeyFactory.getInstance(keyAlgo);
return keyFactory.generatePrivate(keySpec);
}

/**
* Decrypts the password protected contents using the algorithm and IV that is specified in the
* PEM Headers of the file
*
* @param pemHeaders The Proc-Type and DEK-Info PEM headers that have been extracted from the
* key file
* @param keyContents The key as a base64 encoded String
* @param passwordSupplier A password supplier for the encrypted (password protected) key
* @return the decrypted key bytes
* @throws GeneralSecurityException if the key can't be decrypted
* @throws IOException if the PEM headers are missing or malformed
*/
private static byte[] possiblyDecryptPKCS1Key(
Map<String, String> pemHeaders, String keyContents, Supplier<char[]> passwordSupplier)
throws GeneralSecurityException, IOException {
byte[] keyBytes = Base64.getDecoder().decode(keyContents);
String procType = pemHeaders.get("Proc-Type");
if ("4,ENCRYPTED".equals(procType)) {
// We only handle PEM encryption
String encryptionParameters = pemHeaders.get("DEK-Info");
if (null == encryptionParameters) {
// malformed pem
throw new IOException("Malformed PEM File, DEK-Info header is missing");
}
char[] password = passwordSupplier.get();
if (password == null) {
throw new IOException("cannot read encrypted key without a password");
}
Cipher cipher = getCipherFromParameters(encryptionParameters, password);
return cipher.doFinal(keyBytes);
}
return keyBytes;
}

/**
* Creates a {@link Cipher} from the contents of the DEK-Info header of a PEM file. RFC 1421
* indicates that supported algorithms are defined in RFC 1423. RFC 1423 only defines DES-CBS
* and triple DES (EDE) in CBC mode. AES in CBC mode is also widely used though ( 3 different
* variants of 128, 192, 256 bit keys )
*
* @param dekHeaderValue The value of the DEK-Info PEM header
* @param password The password with which the key is encrypted
* @return a cipher of the appropriate algorithm and parameters to be used for decryption
* @throws GeneralSecurityException if the algorithm is not available in the used security
* provider, or if the key is inappropriate for the cipher
* @throws IOException if the DEK-Info PEM header is invalid
*/
private static Cipher getCipherFromParameters(String dekHeaderValue, char[] password)
throws GeneralSecurityException, IOException {
final String padding = "PKCS5Padding";
final SecretKey encryptionKey;
final String[] valueTokens = dekHeaderValue.split(",");
if (valueTokens.length != 2) {
throw new IOException("Malformed PEM file, DEK-Info PEM header is invalid");
}
final String algorithm = valueTokens[0];
final String ivString = valueTokens[1];
final byte[] iv;
try {
iv = hexStringToByteArray(ivString);
} catch (IllegalArgumentException e) {
throw new IOException("Malformed PEM file, DEK-Info IV is invalid", e);
}
if ("DES-CBC".equals(algorithm)) {
byte[] key = generateOpenSslKey(password, iv, 8);
encryptionKey = new SecretKeySpec(key, "DES");
} else if ("DES-EDE3-CBC".equals(algorithm)) {
byte[] key = generateOpenSslKey(password, iv, 24);
encryptionKey = new SecretKeySpec(key, "DESede");
} else if ("AES-128-CBC".equals(algorithm)) {
byte[] key = generateOpenSslKey(password, iv, 16);
encryptionKey = new SecretKeySpec(key, "AES");
} else if ("AES-192-CBC".equals(algorithm)) {
byte[] key = generateOpenSslKey(password, iv, 24);
encryptionKey = new SecretKeySpec(key, "AES");
} else if ("AES-256-CBC".equals(algorithm)) {
byte[] key = generateOpenSslKey(password, iv, 32);
encryptionKey = new SecretKeySpec(key, "AES");
} else {
throw new GeneralSecurityException(
"Private Key encrypted with unsupported algorithm [" + algorithm + "]");
}
String transformation = encryptionKey.getAlgorithm() + "/" + "CBC" + "/" + padding;
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(Cipher.DECRYPT_MODE, encryptionKey, new IvParameterSpec(iv));
return cipher;
}

/**
* Performs key stretching in the same manner that OpenSSL does. This is basically a KDF that
* uses n rounds of salted MD5 (as many times as needed to get the necessary number of key
* bytes)
*
* <p>https://www.openssl.org/docs/man1.1.0/crypto/PEM_write_bio_PrivateKey_traditional.html
*/
private static byte[] generateOpenSslKey(char[] password, byte[] salt, int keyLength) {
byte[] passwordBytes = toUtf8Bytes(password);
try {
MessageDigest md5 = MessageDigest.getInstance("md5");
byte[] key = new byte[keyLength];
int copied = 0;
int remaining;
while (copied < keyLength) {
remaining = keyLength - copied;
md5.update(passwordBytes, 0, passwordBytes.length);
md5.update(salt, 0, 8); // AES IV (salt) is longer but we only need 8 bytes
byte[] tempDigest = md5.digest();
int bytesToCopy = Math.min(remaining, 16); // MD5 digests are 16 bytes
System.arraycopy(tempDigest, 0, key, copied, bytesToCopy);
copied += bytesToCopy;
if (remaining == 0) {
break;
}
md5.update(tempDigest, 0, 16); // use previous round digest as IV
}
Arrays.fill(passwordBytes, (byte) 0);
return key;
} catch (NoSuchAlgorithmException e) {
throw new SslConfigException(
"unexpected exception creating MessageDigest instance for [md5]", e);
}
}

public static byte[] toUtf8Bytes(char[] chars) {
final CharBuffer charBuffer = CharBuffer.wrap(chars);
final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
final byte[] bytes;
if (byteBuffer.hasArray()) {
// there is no guarantee that the byte buffers backing array is the right size
// so we need to make a copy
bytes =
Arrays.copyOfRange(
byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
} else {
final int length = byteBuffer.limit() - byteBuffer.position();
bytes = new byte[length];
byteBuffer.get(bytes);
// if the buffer is not read only we can reset and fill with 0's
if (!byteBuffer.isReadOnly()) {
byteBuffer.clear(); // reset
for (int i = 0; i < byteBuffer.limit(); i++) {
byteBuffer.put((byte) 0);
}
}
}
return bytes;
}

/** Converts a hexadecimal string to a byte array */
private static byte[] hexStringToByteArray(String hexString) {
int len = hexString.length();
if (len % 2 == 0) {
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
final int k = Character.digit(hexString.charAt(i), 16);
final int l = Character.digit(hexString.charAt(i + 1), 16);
if (k == -1 || l == -1) {
throw new IllegalStateException(
"String [" + hexString + "] is not hexadecimal");
}
data[i / 2] = (byte) ((k << 4) + l);
}
return data;
} else {
throw new IllegalStateException(
"Hexadecimal string ["
+ hexString
+ "] has odd length and cannot be converted to a byte array");
}
}

/**
* Parses a DER encoded EC key to an {@link ECPrivateKeySpec} using a minimal {@link DerParser}
*
* @param keyBytes the private key raw bytes
* @return {@link ECPrivateKeySpec}
* @throws IOException if the DER encoded key can't be parsed
*/
private static ECPrivateKeySpec parseEcDer(byte[] keyBytes)
throws IOException, GeneralSecurityException {
DerParser parser = new DerParser(keyBytes);
DerParser.Asn1Object sequence = parser.readAsn1Object();
parser = sequence.getParser();
parser.readAsn1Object().getInteger(); // version
String keyHex = parser.readAsn1Object().getString();
BigInteger privateKeyInt = new BigInteger(keyHex, 16);
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
AlgorithmParameterSpec prime256v1ParamSpec = new ECGenParameterSpec("secp256r1");
keyPairGenerator.initialize(prime256v1ParamSpec);
ECParameterSpec parameterSpec =
((ECKey) keyPairGenerator.generateKeyPair().getPrivate()).getParams();
return new ECPrivateKeySpec(privateKeyInt, parameterSpec);
}

/**
* Parses a DER encoded RSA key to a {@link RSAPrivateCrtKeySpec} using a minimal {@link
* DerParser}
*
* @param keyBytes the private key raw bytes
* @return {@link RSAPrivateCrtKeySpec}
* @throws IOException if the DER encoded key can't be parsed
*/
private static RSAPrivateCrtKeySpec parseRsaDer(byte[] keyBytes) throws IOException {
DerParser parser = new DerParser(keyBytes);
DerParser.Asn1Object sequence = parser.readAsn1Object();
parser = sequence.getParser();
parser.readAsn1Object()
.getInteger(); // (version) We don't need it but must read to get to modulus
BigInteger modulus = parser.readAsn1Object().getInteger();
BigInteger publicExponent = parser.readAsn1Object().getInteger();
BigInteger privateExponent = parser.readAsn1Object().getInteger();
BigInteger prime1 = parser.readAsn1Object().getInteger();
BigInteger prime2 = parser.readAsn1Object().getInteger();
BigInteger exponent1 = parser.readAsn1Object().getInteger();
BigInteger exponent2 = parser.readAsn1Object().getInteger();
BigInteger coefficient = parser.readAsn1Object().getInteger();
return new RSAPrivateCrtKeySpec(
modulus,
publicExponent,
privateExponent,
prime1,
prime2,
exponent1,
exponent2,
coefficient);
}

/**
* Parses a DER encoded DSA key to a {@link DSAPrivateKeySpec} using a minimal {@link DerParser}
*
* @param keyBytes the private key raw bytes
* @return {@link DSAPrivateKeySpec}
* @throws IOException if the DER encoded key can't be parsed
*/
private static DSAPrivateKeySpec parseDsaDer(byte[] keyBytes) throws IOException {
DerParser parser = new DerParser(keyBytes);
DerParser.Asn1Object sequence = parser.readAsn1Object();
parser = sequence.getParser();
parser.readAsn1Object()
.getInteger(); // (version) We don't need it but must read to get to p
BigInteger p = parser.readAsn1Object().getInteger();
BigInteger q = parser.readAsn1Object().getInteger();
BigInteger g = parser.readAsn1Object().getInteger();
parser.readAsn1Object().getInteger(); // we don't need x
BigInteger x = parser.readAsn1Object().getInteger();
return new DSAPrivateKeySpec(x, p, q, g);
}

/**
* Parses a DER encoded private key and reads its algorithm identifier Object OID.
*
* @param keyBytes the private key raw bytes
* @return A string identifier for the key algorithm (RSA, DSA, or EC)
* @throws GeneralSecurityException if the algorithm oid that is parsed from ASN.1 is unknown
* @throws IOException if the DER encoded key can't be parsed
*/
private static String getKeyAlgorithmIdentifier(byte[] keyBytes)
throws IOException, GeneralSecurityException {
DerParser parser = new DerParser(keyBytes);
DerParser.Asn1Object sequence = parser.readAsn1Object();
parser = sequence.getParser();
parser.readAsn1Object().getInteger(); // version
DerParser.Asn1Object algSequence = parser.readAsn1Object();
parser = algSequence.getParser();
String oidString = parser.readAsn1Object().getOid();
switch (oidString) {
case "1.2.840.10040.4.1":
return "DSA";
case "1.2.840.113549.1.1.1":
return "RSA";
case "1.2.840.10045.2.1":
return "EC";
}
throw new GeneralSecurityException(
"Error parsing key algorithm identifier. Algorithm with OID ["
+ oidString
+ "] is not supported");
}

static List<Certificate> readCertificates(Collection<Path> certPaths)
throws CertificateException, IOException {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
List<Certificate> certificates = new ArrayList<>(certPaths.size());
for (Path path : certPaths) {
try (InputStream input = Files.newInputStream(path)) {
final Collection<? extends Certificate> parsed =
certFactory.generateCertificates(input);
if (parsed.isEmpty()) {
throw new SslConfigException(
"failed to parse any certificates from ["
+ path.toAbsolutePath()
+ "]");
}
certificates.addAll(parsed);
}
}
return certificates;
}
}
  1. 接下来要对keystore的SSLContext进行解析,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

/**
* KeyStoreUtil for handle {@link SSLContext} storage facility
*
*/
final class KeyStoreUtil {

private KeyStoreUtil() {
throw new IllegalStateException("Utility class should not be instantiated");
}

static String inferKeyStoreType(Path path) {
String name = path == null ? "" : path.toString().toLowerCase(Locale.ROOT);
if (name.endsWith(".p12") || name.endsWith(".pfx") || name.endsWith(".pkcs12")) {
return "PKCS12";
} else {
return "jks";
}
}

static KeyStore readKeyStore(Path path, String type, char[] password)
throws GeneralSecurityException {
if (Files.notExists(path)) {
throw new SslConfigException(
"cannot read a ["
+ type
+ "] keystore from ["
+ path.toAbsolutePath()
+ "] because the file does not exist");
}
try {
KeyStore keyStore = KeyStore.getInstance(type);
try (InputStream in = Files.newInputStream(path)) {
keyStore.load(in, password);
}
return keyStore;
} catch (IOException e) {
throw new SslConfigException(
"cannot read a ["
+ type
+ "] keystore from ["
+ path.toAbsolutePath()
+ "] - "
+ e.getMessage(),
e);
}
}

/** Construct an in-memory keystore with a single key entry. */
static KeyStore buildKeyStore(
Collection<Certificate> certificateChain, PrivateKey privateKey, char[] password)
throws GeneralSecurityException {
KeyStore keyStore = buildNewKeyStore();
keyStore.setKeyEntry(
"key", privateKey, password, certificateChain.toArray(new Certificate[0]));
return keyStore;
}

/** Construct an in-memory keystore with multiple trusted cert entries. */
static KeyStore buildTrustStore(Iterable<Certificate> certificates)
throws GeneralSecurityException {
assert certificates != null : "Cannot create keystore with null certificates";
KeyStore store = buildNewKeyStore();
int counter = 0;
for (Certificate certificate : certificates) {
store.setCertificateEntry("cert-" + counter, certificate);
counter++;
}
return store;
}

static KeyStore buildDefaultStore(
Path keyStorePath, Supplier<char[]> passwordSupplier, String keyStoreType)
throws GeneralSecurityException, IOException {
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(
(keyStorePath.getFileSystem().provider().newInputStream(keyStorePath)),
passwordSupplier.get());
return keyStore;
}

private static KeyStore buildNewKeyStore() throws GeneralSecurityException {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try {
keyStore.load(null, null);
} catch (IOException e) {
throw new SslConfigException(
"Unexpected error initializing a new in-memory keystore", e);
}
return keyStore;
}

/**
* Creates a {@link X509ExtendedKeyManager} based on the key material in the provided {@link
* KeyStore}
*/
static X509ExtendedKeyManager createKeyManager(
KeyStore keyStore, char[] password, String algorithm) throws GeneralSecurityException {
KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
kmf.init(keyStore, password);
KeyManager[] keyManagers = kmf.getKeyManagers();
for (KeyManager keyManager : keyManagers) {
if (keyManager instanceof X509ExtendedKeyManager) {
return (X509ExtendedKeyManager) keyManager;
}
}
throw new SslConfigException(
"failed to find a X509ExtendedKeyManager in the key manager factory for ["
+ algorithm
+ "] and keystore ["
+ keyStore
+ "]");
}

/**
* Creates a {@link X509ExtendedTrustManager} based on the trust material in the provided {@link
* KeyStore}
*/
static X509ExtendedTrustManager createTrustManager(
@Nullable KeyStore trustStore, String algorithm)
throws NoSuchAlgorithmException, KeyStoreException {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm);
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
for (TrustManager trustManager : trustManagers) {
if (trustManager instanceof X509ExtendedTrustManager) {
return (X509ExtendedTrustManager) trustManager;
}
}
throw new SslConfigException(
"failed to find a X509ExtendedTrustManager in the trust manager factory for ["
+ algorithm
+ "] and truststore ["
+ trustStore
+ "]");
}
}
  1. 读取的私钥是个ASN.1数据,解析其内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.Objects;

/**
* A bare-minimum ASN.1 DER decoder, just having enough functions to decode PKCS#1 private keys in
* order to remain JCE/JVM agnostic.
*
* <p>Based on https://github.com/groovenauts/jmeter_oauth_plugin/blob/master/jmeter/src/
* main/java/org/apache/jmeter/protocol/oauth/sampler/PrivateKeyReader.java
*
*/
final class DerParser {
// Constructed Flag
private static final int CONSTRUCTED = 0x20;

// Tag and data types
private static final int INTEGER = 0x02;
private static final int OCTET_STRING = 0x04;
private static final int OBJECT_OID = 0x06;
private static final int NUMERIC_STRING = 0x12;
private static final int PRINTABLE_STRING = 0x13;
private static final int VIDEOTEX_STRING = 0x15;
private static final int IA5_STRING = 0x16;
private static final int GRAPHIC_STRING = 0x19;
private static final int ISO646_STRING = 0x1A;
private static final int GENERAL_STRING = 0x1B;

private static final int UTF8_STRING = 0x0C;
private static final int UNIVERSAL_STRING = 0x1C;
private static final int BMP_STRING = 0x1E;

private final InputStream derInputStream;
private final int maxAsnObjectLength;

DerParser(byte[] bytes) {
this.derInputStream = new ByteArrayInputStream(bytes);
this.maxAsnObjectLength = bytes.length;
}

Asn1Object readAsn1Object() throws IOException {
int tag = derInputStream.read();
if (tag == -1) {
throw new IOException("Invalid DER: stream too short, missing tag");
}
int length = getLength();
// getLength() can return any 32 bit integer, so ensure that a corrupted encoding won't
// force us into allocating a very large array
if (length > maxAsnObjectLength) {
throw new IOException(
"Invalid DER: size of ASN.1 object to be parsed appears to be larger than the size of the key file "
+ "itself.");
}
byte[] value = new byte[length];
int n = derInputStream.read(value);
if (n < length) {
throw new IOException(
"Invalid DER: stream too short, missing value. "
+ "Could only read "
+ n
+ " out of "
+ length
+ " bytes");
}
return new Asn1Object(tag, length, value);
}

/**
* Decode the length of the field. Can only support length encoding up to 4 octets.
*
* <p>In BER/DER encoding, length can be encoded in 2 forms:
*
* <ul>
* <li>Short form. One octet. Bit 8 has value "0" and bits 7-1 give the length.
* <li>Long form. Two to 127 octets (only 4 is supported here). Bit 8 of first octet has value
* "1" and bits 7-1 give the number of additional length octets. Second and following
* octets give the length, base 256, most significant digit first.
* </ul>
*
* @return The length as integer
*/
private int getLength() throws IOException {

int i = derInputStream.read();
if (i == -1) throw new IOException("Invalid DER: length missing");

// A single byte short length
if ((i & ~0x7F) == 0) return i;

int num = i & 0x7F;

// We can't handle length longer than 4 bytes
if (i >= 0xFF || num > 4)
throw new IOException("Invalid DER: length field too big (" + i + ")"); // $NON-NLS-1$

byte[] bytes = new byte[num];
int n = derInputStream.read(bytes);
if (n < num) throw new IOException("Invalid DER: length too short");

return new BigInteger(1, bytes).intValue();
}

/**
* An ASN.1 TLV. The object is not parsed. It can only handle integers.
*
* @author zhang
*/
static class Asn1Object {

protected final int type;
protected final int length;
protected final byte[] value;
protected final int tag;

/**
* Construct a ASN.1 TLV. The TLV could be either a constructed or primitive entity.
*
* <p>The first byte in DER encoding is made of following fields:
*
* <pre>
* -------------------------------------------------
* |Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1|
* -------------------------------------------------
* | Class | CF | + Type |
* -------------------------------------------------
* </pre>
*
* <ul>
* <li>Class: Universal, Application, Context or Private
* <li>CF: Constructed flag. If 1, the field is constructed.
* <li>Type: This is actually called tag in ASN.1. It indicates data type (Integer,
* String) or a construct (sequence, choice, set).
* </ul>
*
* @param tag Tag or Identifier
* @param length Length of the field
* @param value Encoded octet string for the field.
*/
Asn1Object(int tag, int length, byte[] value) {
this.tag = tag;
this.type = tag & 0x1F;
this.length = length;
this.value = value;
}

public int getType() {
return type;
}

public int getLength() {
return length;
}

public byte[] getValue() {
return value;
}

public boolean isConstructed() {
return (tag & DerParser.CONSTRUCTED) == DerParser.CONSTRUCTED;
}

/**
* For constructed field, return a parser for its content.
*
* @return A parser for the construct.
*/
public DerParser getParser() throws IOException {
if (!isConstructed())
throw new IOException("Invalid DER: can't parse primitive entity"); // $NON-NLS-1$

return new DerParser(value);
}

/**
* Get the value as integer
*
* @return BigInteger
*/
public BigInteger getInteger() throws IOException {
if (type != DerParser.INTEGER)
throw new IOException("Invalid DER: object is not integer"); // $NON-NLS-1$

return new BigInteger(value);
}

public String getString() throws IOException {

String encoding;

switch (type) {
case DerParser.OCTET_STRING:
// octet string is basically a byte array
return toHexString(value);
case DerParser.NUMERIC_STRING:
case DerParser.PRINTABLE_STRING:
case DerParser.VIDEOTEX_STRING:
case DerParser.IA5_STRING:
case DerParser.GRAPHIC_STRING:
case DerParser.ISO646_STRING:
case DerParser.GENERAL_STRING:
encoding = "ISO-8859-1"; // $NON-NLS-1$
break;

case DerParser.BMP_STRING:
encoding = "UTF-16BE"; // $NON-NLS-1$
break;

case DerParser.UTF8_STRING:
encoding = "UTF-8"; // $NON-NLS-1$
break;

case DerParser.UNIVERSAL_STRING:
throw new IOException("Invalid DER: can't handle UCS-4 string"); // $NON-NLS-1$

default:
throw new IOException("Invalid DER: object is not a string"); // $NON-NLS-1$
}

return new String(value, encoding);
}

public String getOid() throws IOException {

if (type != DerParser.OBJECT_OID) {
throw new IOException("Ivalid DER: object is not object OID");
}
StringBuilder sb = new StringBuilder(64);
switch (value[0] / 40) {
case 0:
sb.append('0');
break;
case 1:
sb.append('1');
value[0] -= 40;
break;
default:
sb.append('2');
value[0] -= 80;
break;
}
int oidPart = 0;
for (int i = 0; i < length; i++) {
oidPart = (oidPart << 7) + (value[i] & 0x7F);
if ((value[i] & 0x80) == 0) {
sb.append('.');
sb.append(oidPart);
oidPart = 0;
}
}

return sb.toString();
}
}

private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();

private static String toHexString(byte[] bytes) {
Objects.requireNonNull(bytes);
StringBuilder sb = new StringBuilder(2 * bytes.length);

for (byte b : bytes) {
sb.append(HEX_DIGITS[b >> 4 & 0xf]).append(HEX_DIGITS[b & 0xf]);
}

return sb.toString();
}
}
  1. 要对其内容进行测试,我们可以模拟一个服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

import org.eclipse.jetty.server.Server;

class HttpsMockServer {

@Override
protected void configConnectors(Server mockServer) {
//config ssl
URL url = Thread.currentThread().getContextClassLoader().getResource("server.keystore.jks");
SslContextFactory sslContextFactory = new SslContextFactory();
try {
String path = java.net.URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8.name());
sslContextFactory.setKeyStorePath(path);
sslContextFactory.setKeyStorePassword("ericsson");
sslContextFactory.setKeyManagerPassword("ericsson");
} catch (Exception e) {
e.printStackTrace();
}

HttpConfiguration httpsConfig = new HttpConfiguration();
httpsConfig.setSecurePort(HTTPS_PORT);
httpsConfig.setSecureScheme("https");
httpsConfig.addCustomizer(new SecureRequestCustomizer());

ServerConnector sslConnector = new ServerConnector(mockServer, new SslConnectionFactory(sslContextFactory,
HttpVersion.HTTP_1_1.asString()), new HttpConnectionFactory(httpsConfig));
sslConnector.setPort(HTTPS_PORT);
mockServer.addConnector(sslConnector);
}
}

然后用我们分别用客户端生成的client.keystore.jksclient.pem/client.key进行测试即可。