This repository has been archived by the owner on Sep 21, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Acme2J.java
1719 lines (1523 loc) · 75.5 KB
/
Acme2J.java
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
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import java.io.*;
import java.net.IDN;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/************************* 本程序实现了通配符证书申请功能,使用dns-txt验证 ***********************************
* ---------------------------------------------------------------------------------------------------------------------
* 使用前须知:
* 本程序实现了acme.sh/3.0.8(https://github.com/acmesh-official/acme.sh)的默认dns申请证书流程,
* CA为zerossl(zerossl.com),账户私钥和域名私钥默认使用ecc-prime256v1生成,暂不支持其他加密形式
* 获取的证书包含 yourdomain.com 和 *.yourdomain.com
*
* 运行要求: java 8+、curl、openssl
* 运行目录请使用全英文路径,不要有空格
* 密钥、签名等功能由openssl(openssl.org & slproweb.com/products/Win32OpenSSL.html)实现,
* 网络请求由curl(curl.se)实现。java调用系统命令行实现必要功能。
*
* 产生的账户和域名配置文件可用于acme.sh
*
* 注意:
* 本程序不支持ip,不支持多域名
* 本程序未遵循acme.sh的设计,
* 本程序未经过严格测试与优化,
* 禁止滥用,
* 禁止其他一切损害公共利益的行为。
*
* Read before use:
* This program implements the default certificate application process of acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh), using dns-txt,
* The CA is zerossl (zerossl.com), and the account private key and domain private key are generated by ecc-prime256v1 by default. Other encryption forms are not supported at present
* The certificate supports both yourdomain.com and *.yourdomain.com
*
* Running requirements: java 8+, curl, openssl
* Please use the full English path for the running directory without spaces
* Key, signature and other functions are implemented by openssl (openssl.org & slproweb.com/products/Win32OpenSSL.html),
* Network requests are implemented by curl (curl.se). Java calls the system command to implement necessary functions.
*
* The produced account and domain configuration files can be used for acme.sh
*
* Notice:
* This program does not support IP, multi domain are not supported
* This program does not follow the design of acme.sh,
* This program has not been strictly tested and optimized,
* Abuse is prohibited,
* Any other acts that harm the public interest are prohibited.
*---------------------------------------------------------------------------------------------------------------------
*
* @author ssldog.com
******************************************************************************************/
public class Acme2J {
/**
* 使用acme.sh的默认配置
*/
public static final String DEFAULT_CA = "zerossl";
public static final String DEFAULT_TYPE = "dns";
public static final String DEFAULT_ACCOUNT_KEY_LENGTH = "ec-256";
public static final String DEFAULT_DOMAIN_KEY_LENGTH = "ec-256";
public static final String ECC_NAME = "prime256v1";
public static final String ECC_KEY_LEN = "256";
public static final String CA_ZEROSSL = "https://acme.zerossl.com/v2/DV90";
public static final String ZERO_EAB_ENDPOINT = "https://api.zerossl.com/acme/eab-credentials-email";
/**
* 必要的
*/
public static String OPENSSL = new File(windows() ? "openssl/bin/openssl.exe" : "/usr/bin/openssl").getAbsolutePath();
public static String CURL = new File(windows() ? "curl/bin/curl.exe" : "/usr/bin/curl").getAbsolutePath();
/**
* account.key
*/
public static String ACCOUNT_KEY = new File("account.key").getAbsolutePath();
/**
* account.json
*/
public static String ACCOUNT_JSON = new File("account.json").getAbsolutePath();
/**
* ca.conf
*/
public static String EMAIL = "thankyou@" + UUID.randomUUID().toString().split("-")[0] + ".com"; // 如果不输入 email=youremail,则随机生成
public static String CA_CONF = new File("ca.conf").getAbsolutePath();
public static String ACCOUNT_URL = ""; // 在申请证书时要用
public static String EAB_KEY_ID = "";
public static String EAB_HMAC_KEY = "";
/**
* 输入 domain=MAIN_DOMAIN
*/
public static String MAIN_DOMAIN = ""; // 默认 yourdomain.com,如果输入 domain=*.yourdomain.com,则 ALT_DOMAIN=yourdomain.com
public static String ALT_DOMAIN = ""; // 默认*。yourdomain.com
/**
* 域名证书申请相关文件
*/
public static String CERT_KEY_PATH = ""; // ./domain/domain.key;
public static String CSR_PATH = ""; // ./domain/domain.csr
public static String CSR_CONF_PATH = ""; // ./domain/domain.csr.conf // acme.sh -> DOMAIN_SSL_CONF
public static String DOMAIN_CONF_PATH = ""; // ./domain/domain.conf";
public static String CA_CERT_PATH = ""; // ./domain/ca.cer
public static String DOMAIN_CER_PATH = ""; // ./domain/domain.cer // acme.sh -> CERT_PATH
public static String FULLCHAIN_CER_PATH = ""; // ./domain/fullchain.cer // acme.sh -> CERT_FULLCHAIN_PATH
/**
* zerosslAcmeApi
*/
public static String NEW_NONCE = "https://acme.zerossl.com/v2/DV90/newNonce";
public static String NEW_ACCOUNT = "https://acme.zerossl.com/v2/DV90/newAccount";
public static String NEW_ORDER = "https://acme.zerossl.com/v2/DV90/newOrder";
public static String REVOKE_CERT = "https://acme.zerossl.com/v2/DV90/revokeCert";
public static String KEY_CHANGE = "https://acme.zerossl.com/v2/DV90/keyChange";
/**
* jwk信息不变
*/
public static String JWK = "";
// String JWK_HEADER = String.format("\"{\"alg\": \"ES%s\", \"jwk\": %s}\"",ECC_KEY_LEN,jwk)
/**
* 域名验证时共享
*/
public static String SHARED_NONCE = "";
/**
* 域名txt记录
*/
public static Map<String, String> authrsForDomain = new LinkedHashMap<>(); // 不要使用hashmap,会打乱顺序,Le_Vlist,subjectAltName会受影响,暂时不知道会产生什么问题,总之不要动它
public static Map<String,String> txtForDomain = new LinkedHashMap<>();
public static Map<String, String> challsForDomain = new LinkedHashMap<>();
/**
* thumbprint 信息不变
*/
public static String thumbprint = "";
/** ************************** 一个完整的证书申请流程,使用 dns-txt 验证 *************************************************
*
* 1 checkRequiredExe() 检查运行环境要求,检查系统os,检查curl和openssl,配置其路径
* 3 initZerosslApi() 获取zerosslApi,配置nonce、account、order等相关api
* 2 createAccountKey()&calcJwk() 创建账户私钥,保存到 account.key,然后得到 jwk
* 4 getEabKid() 提交邮箱,获取eab_kid,保存email、eab_key_id、account_url等信息到 ca.conf
* 5 regAccount() 注册账户,提交公钥和eab_kid。获得账户信息,保存到 account.json
* 6 initDomainInfo(args) 配置域名相关信息
* 7 createDomainKey()&onBeforeIssue() 执行申请证书前的操作,创建域名私钥./domainName/domainName.key,保存申请域名的配置到./domain/domain.conf
* 8 sendNewOrder() 开始申请证书,提交 id、domain 等信息,获取 authorizations url,保存到 authrsForDomain 中。 acme.sh => STEP 1, Ordering a Certificate
* 9 getEachAuthorizations() 获取每个域名token,token和thumbprint组成KEY_AUTHORIZATION;保存 Le_Vlist(包含challengeUrl) 到 domain.conf 。 acme.sh => STEP 2, Get the authorizations of each domain
* 10 continueVerify(DOMAIN_CONF) 继续验证dns记录,完成申请证书
* 11 createCsr() 创建 domain.csr acme.sh => if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then
* 12 finalizeOrder() 完成申请证书最后一步 acme.sh => Lets finalize the order. > Order status is processing, lets sleep and retry. > Order status is valid.
* 13 downloadCert() 下载证书
* 14 extractCert() 非必要,没有实现
* ************************************************************************************************************** */
public static void main(String[] args) throws Exception {
System.out.println(Arrays.toString(args));
// java Acme2J issue domain=yourdomain.com [email protected]
if ("issue".equals(args[0])) {
System.out.println("申请域名证书");
if (new File(IDN.toASCII(valueFor("domain", "=", Arrays.toString(args).replaceAll("[\\[\\]\\s]","")))).exists()) {
System.out.println("域名目录已存在");
System.exit(1);
}
checkRequiredExe();
initZerosslApi();
createAccountKey();
calcJwk(ACCOUNT_KEY);
getEabKid(getEmail(args));
regAccount();
initDomainInfo(args);
createDomainKey();
onBeforeIssue();
sendNewOrder();
getEachAuthorizations();
System.out.println("NEXT: "+Arrays.toString(args).replace(",","").replace("issue", "continue"));
// java Acme2J continue domain=yourdomain.com [email protected]
}else if("continue".equals(args[0])){
System.out.println("验证dns记录,继续申请");
initDomainInfo(args);
continueVerify(DOMAIN_CONF_PATH);
createCrt(CSR_CONF_PATH,CSR_PATH);
finalizeOrder(DOMAIN_CONF_PATH, CSR_PATH);
downloadCert(DOMAIN_CONF_PATH, DOMAIN_CER_PATH); // DOMAIN_CER -> FULLCHAIN_CER 指定完整证书保存的地方
// renew -> issue
} else if ("renew".equals(args[0])) {
System.out.println("更新域名证书");
initDomainInfo(args);
new File(DOMAIN_CONF_PATH).renameTo(new File(DOMAIN_CONF_PATH + "." +new Date().toInstant().toString().replace(":", "")));
createDomainKey();
onBeforeIssue();
sendNewOrder();
getEachAuthorizations();
System.out.println("NEXT: "+Arrays.toString(args).replace(",","").replace("issue", "continue"));
} else {
System.out.println("未知命令,暂时只支持申请证书");
}
// // account
// checkRequiredExe();
// initZerosslApi();
// createAccountKey();
// calcJwk(ACCOUNT_KEY);
// getEabKid(getEmail(args));
// regAccount();
//
// // domain
// initDomainInfo(args);
// createDomainKey();
// onBeforeIssue();
// sendNewOrder();
// getEachAuthorizations();
// continue
// initDomainInfo(args);
// continueVerify(DOMAIN_CONF);
// createCrt(CSR_PATH,CSR_CONF_PATH);
// finalizeOrder(DOMAIN_CONF, CSR_PATH);
// downloadCert(DOMAIN_CONF,DOMAIN_CER);
//
}
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 流程方法 开始 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
/**
* 检查运行环境
*/
public static void checkRequiredExe() throws Exception {
String curlConf = readConf("curl", "Acme2J.conf");
String opensslConf = readConf("openssl", "Acme2J.conf");
CURL = curlConf.isEmpty() ? CURL : curlConf;
OPENSSL = opensslConf.isEmpty() ? OPENSSL : opensslConf;
if (!new File(CURL).exists()) {
System.out.println("缺少运行环境: curl");
System.exit(1);
}
if (!new File(OPENSSL).exists()) {
System.out.println("缺少运行环境: openssl");
System.exit(1);
}
System.out.println("curl=" + CURL);
System.out.println("openssl=" + OPENSSL);
}
/**
* 初始化域名信息和目录,暂不支持idn
* @param args
* @throws Exception
*/
public static void initDomainInfo(String[] args) throws Exception {
String domain = Arrays.stream(args)
.filter(arg -> arg.contains("domain=") && arg.length() > 9)
.findFirst().orElse("domain=defaultNull")
.split("=")[1]
.replace("*", "_all_all_all_all"); // 替换通配符,方便判断域名格式是否正确
domain = IDN.toASCII(domain);
System.out.println("your domain: " + domain);
if (domain.matches("^(?:[_a-z0-9](?:[_a-z0-9-]{0,61}[a-z0-9])?\\.)+(?:[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?)?$")) {
if (domain.startsWith("_all_all_all_all")) {
domain = domain.replace("_all_all_all_all", "*");
MAIN_DOMAIN = domain;
ALT_DOMAIN = domain.replace("*.", "");
} else {
MAIN_DOMAIN = domain;
ALT_DOMAIN = "*." + domain;
}
} else {
System.out.println("请输入正确域名!");
System.exit(1);
}
System.out.println("MAIN_DOMAIN: " + MAIN_DOMAIN);
System.out.println("ALT_DOMAIN: " + ALT_DOMAIN);
String rootDomain = MAIN_DOMAIN.replace("*.", "").trim();
new File(rootDomain).mkdirs();
CERT_KEY_PATH = new File(rootDomain + "/" + rootDomain + ".key").getAbsolutePath();
CSR_PATH =new File( rootDomain + "/" + rootDomain + ".csr").getAbsolutePath();
CSR_CONF_PATH =new File( rootDomain + "/" + rootDomain + ".csr.conf").getAbsolutePath();
DOMAIN_CONF_PATH =new File( rootDomain + "/" + rootDomain + ".conf").getAbsolutePath();
CA_CERT_PATH =new File( rootDomain + "/ca.cer").getAbsolutePath();
DOMAIN_CER_PATH = new File(rootDomain + "/" + rootDomain + ".cer").getAbsolutePath();
FULLCHAIN_CER_PATH =new File( rootDomain + "/fullchian.cer").getAbsolutePath();
}
/**
* 创建account.key文件
*
* @throws Exception
*/
public static void createAccountKey() throws Exception {
File accountKey = new File(ACCOUNT_KEY);
if (accountKey.exists() && accountKey.length() != 0) {
System.out.println("使用已有account.key");
return;
}
// TODO acme.sh: openssl -> openssl ecparam -name prime256v1 -noout -genkey -out account.key
String[] commands = {
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
OPENSSL +
" ecparam -name prime256v1 -noout -genkey -out " + ACCOUNT_KEY
};
exec_(commands);
if (accountKey.exists() && accountKey.length() != 0) {
System.out.println("ACCOUNT_KEY = " + ACCOUNT_KEY);
System.out.println("account.key创建成功");
} else {
System.out.println("account.key创建失败");
System.exit(1);
}
}
/**
* 获取zerossl接口信息
*
* @throws Exception
*/
public static void initZerosslApi() throws Exception {
// TODO curl -k -i --raw -H "user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)" -H "accept: */*" -H "host: acme.zerossl.com" "https://acme.zerossl.com/v2/DV90"
String[] commands = {
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
CURL +
" -k --raw" +
" -H \"user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)\"" +
" -H \"accept: */*\" -H \"host: acme.zerossl.com\"" +
" \"" + CA_ZEROSSL + "\""
};
String response = exec_(commands);
NEW_NONCE = valueFor("newNonce", response);
NEW_ACCOUNT = valueFor("newAccount", response);
NEW_ORDER = valueFor("newOrder", response);
REVOKE_CERT = valueFor("revokeCert", response);
KEY_CHANGE = valueFor("keyChange", response);
System.out.println("NEW_NONCE = " + NEW_NONCE);
System.out.println("NEW_ACCOUNT = " + NEW_ACCOUNT);
System.out.println("NEW_ORDER = " + NEW_ORDER);
System.out.println("REVOKE_CERT = " + REVOKE_CERT);
System.out.println("KEY_CHANGE " + KEY_CHANGE);
}
/**
* 提交邮箱获取eab_kid并保存
*
* @throws Exception
*/
public static void getEabKid(String email) throws Exception {
File caConf = new File(CA_CONF);
if (caConf.exists() && caConf.length() != 0) {
System.out.println("使用已有ca.conf: "+CA_CONF);
return;
}
// TODO curl -k -i --raw -X POST --data-raw "[email protected]" -H "user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)" -H "accept: */*" -H "content-type: application/x-www-form-urlencoded" -H "host: api.zerossl.com" "https://api.zerossl.com/acme/eab-credentials-email"
String[] command = {
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
CURL +
" -k --raw" +
" -X POST --data-raw \"email=" + EMAIL + "\"" +
" -H \"user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)\"" +
" -H \"accept: */*\"" +
" -H \"content-type: application/x-www-form-urlencoded\"" +
" -H \"host: api.zerossl.com\"" +
" \"" + ZERO_EAB_ENDPOINT + "\""
};
String response = exec_(command);
String CA_EMAIL = EMAIL;
String CA_EAB_KEY_ID = valueFor("eab_kid", response);
String CA_EAB_HMAC_KEY = valueFor("eab_hmac_key", response);
System.out.println("CA_EMAIL=" + CA_EMAIL);
System.out.println("CA_EAB_KEY_ID=" + CA_EAB_KEY_ID);
System.out.println("CA_EAB_HMAC_KEY" + CA_EAB_HMAC_KEY);
EAB_HMAC_KEY = CA_EAB_HMAC_KEY;
EAB_KEY_ID = CA_EAB_KEY_ID;
try (FileWriter writer = new FileWriter(caConf, true)) {
writer.write("CA_EMAIL=" + CA_EMAIL + System.lineSeparator());
writer.write("CA_EAB_KEY_ID=" + CA_EAB_KEY_ID + System.lineSeparator());
writer.write("CA_EAB_HMAC_KEY=" + CA_EAB_HMAC_KEY + System.lineSeparator());
// writer.write("path="+caConf.getAbsolutePath().replace("\\","\\\\")+System.lineSeparator());
System.out.println("eab_kid信息写入ca.conf文件成功");
} catch (IOException e) {
System.out.println("eab_kid信息写入ca.conf文件失败");
e.printStackTrace();
}
}
/**
* 获取账户信息
*
* @return newNonce
* @throws Exception
*/
public static void regAccount() throws Exception {
// accountJson
File accountFile = new File(ACCOUNT_JSON);
if (accountFile.exists() && accountFile.length() != 0) {
System.out.println("使用已有account: "+ACCOUNT_JSON);
return;
}
/**
* if [ "$_eab_id" ] && [ "$_eab_hmac_key" ]; then
* eab_protected="{\"alg\":\"HS256\",\"kid\":\"$_eab_id\",\"url\":\"${ACME_NEW_ACCOUNT}\"}"
* _debug3 eab_protected "$eab_protected"
*
* eab_protected64=$(printf "%s" "$eab_protected" | _base64 | _url_replace)
* _debug3 eab_protected64 "$eab_protected64"
*
* eab_payload64=$(printf "%s" "$jwk" | _base64 | _url_replace)
* _debug3 eab_payload64 "$eab_payload64"
*
* eab_sign_t="$eab_protected64.$eab_payload64"
* _debug3 eab_sign_t "$eab_sign_t"
*/
String eabProtected = String.format("{\"alg\":\"HS256\",\"kid\":\"%s\",\"url\":\"%s\"}", EAB_KEY_ID, NEW_ACCOUNT);
String eabProtected64 = ""; // single line | url_re
String eabPayload64 = ""; // eab_payload64 = base( jwk ) # single line | url_re
String eabSignT = ""; // eab_sign_t="$eab_protected64.$eab_payload64"
String keyHex = ""; // key_hex="$(_durl_replace_base64 "$_eab_hmac_key" | _dbase64 | _hex_dump | tr -d ' ')"
String eabSignature = ""; // eab_signature=$(printf "%s" "$eab_sign_t" | _hmac sha256 $key_hex | _base64 | _url_replace)
// -> printf $eab_sign_t | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$key_hex" -binary | openssl base64 | url_re
eabProtected64 = urlReplace(Base64.getEncoder().encodeToString(eabProtected.getBytes()));
eabPayload64 = urlReplace(Base64.getEncoder().encodeToString(JWK.getBytes()));
eabSignT = eabProtected64 + "." + eabPayload64;
System.out.println("eabProtected64: " + eabProtected64);
System.out.println("eabPayload64: " + eabPayload64);
System.out.println("eabSignT: " + eabSignT);
// byte[] decodedBytes = base64Decode(urlBase64("kRFBlMmdDmG6_jL******mFV8G8-83dFj****2lr-5uHxwoxVH-A"));
// System.out.println(decodedBytes.length);
// String key_hex = b2h(decodedBytes);
// System.out.println(key_hex);
byte[] decodeBytes = base64Decode(durlReplaceBase64(EAB_HMAC_KEY));
keyHex = b2h(decodeBytes);
System.out.println("keyHex: " + keyHex);
// TODO acme.sh: openssl -> echo -n "$eabProtected64.$eabPayload64" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$keyHex" -binary | openssl base64 -e -A
String[] command = {
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
windows()
? String.format("set /p=\"%s\" < %s | %s dgst -sha256 -mac HMAC -macopt \"hexkey:%s\" -binary | %s base64",
eabSignT, OPENSSL, OPENSSL, keyHex, OPENSSL)
: String.format("echo -n %s | %s dgst -sha256 -mac HMAC -macopt \"hexkey:%s\" -binary | %s base64",
eabSignT, OPENSSL, keyHex, OPENSSL)
};
String result = exec_(command);
eabSignature = urlReplace(result);
System.out.println("eabSignature: " + eabSignature);
String externalBinding = String.format(
",\"externalAccountBinding\":{\"protected\":\"%s\", \"payload\":\"%s\", \"signature\":\"%s\"}",
eabProtected64, eabPayload64, eabSignature
);
System.out.println("externalBinding: " + externalBinding);
String emailSg = String.format("\"contact\": [\"mailto:%s\"], ", EMAIL);
String regjson = String.format(
"{%s\"termsOfServiceAgreed\": true%s}",
emailSg, externalBinding
);
System.out.println(regjson);
String payload64 = ""; // payload64=$(printf "%s" "$payload" | _base64 | _url_replace)
payload64 = urlReplace(Base64.getEncoder().encodeToString(regjson.getBytes()));
System.out.println("payload64: " + payload64);
// protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}'
// JWK_HEADERPLACE_PART1='{"nonce": "'
// JWK_HEADERPLACE_PART2='", "alg": "ES'$__ECC_KEY_LEN'"'
String protected_ = String.format(
"{\"nonce\": \"%s\", \"url\": \"%s\", \"alg\": \"ES%s\", \"jwk\": %s}",
newNonce(), NEW_ACCOUNT, ECC_KEY_LEN, JWK
);
System.out.println("protected: " + protected_);
String protected64 = urlReplace(Base64.getEncoder().encodeToString(protected_.getBytes()));
System.out.println("protected64: " + protected64);
// 签名
String ecSig = sign(protected64 + "." + payload64, ACCOUNT_KEY, "sha256");
System.out.println("ecSig_: " + ecSig);
String sig = urlReplace(hexStrBase64(ecSig));
System.out.println("sig: " + sig);
String body =
String.format("{\"protected\": \"%s\", \"payload\": \"%s\", \"signature\": \"%s\"}",
protected64, payload64, sig
);
// TODO curl -k -i --raw -X POST --data-raw body -H "user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)" -H "accept: */*" -H "content-type: application/jose+json" -H "host: acme.zerossl.com" "https://acme.zerossl.com/v2/DV90/newAccount"
command = new String[]{
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
CURL +
" -k -i --raw -X POST --data-raw \"" + body.replace("\"", "\\\"") + "\"" +
" -H \"user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)\"" +
" -H \"accept: */*\" -H \"content-type: application/jose+json\"" +
" -H \"host: acme.zerossl.com\"" +
" \"" + NEW_ACCOUNT + "\""
};
String response = exec_(command);
System.out.println("newAccountResponse: " + response);
String newNonce = valueFor("replay-nonce", ":", response);
String accountJson = "";
// accountJson
Matcher value = Pattern.compile("\\s(\\{.*?\\})\\s").matcher(response);
if (!value.find() && value.group(1) == null && "".equals(value.group(1))) {
System.out.println("在 " + response + "\n中找不到 accountJson");
System.out.println("出错了!");
System.exit(1);
} else {
accountJson = value.group(1);
System.out.println("accountJson: " + accountJson);
}
try (FileWriter writer = new FileWriter(accountFile)) {
writer.write(accountJson);
System.out.println("写入account.json文件成功");
} catch (IOException e) {
System.out.println("写入account.json文件失败");
e.printStackTrace();
}
// ca.conf
// ACCOUNT_URL
String accountURL = valueFor("location", ":", response);
ACCOUNT_URL = accountURL;
saveCaConf("ACCOUNT_URL", accountURL);
// CA_KEY_HASH
String caKeyHash = calcAccountKeyHash(ACCOUNT_KEY);
saveCaConf("CA_KEY_HASH", caKeyHash);
SHARED_NONCE = newNonce;
}
/**
* 创建域名私钥
*
* @throws Exception
*/
public static void createDomainKey() throws Exception {
File domainKey = new File(CERT_KEY_PATH);
if (domainKey.exists() && domainKey.length() != 0) {
System.out.println("使用已有domain.key: " + domainKey.getAbsolutePath());
return;
}
// TODO acme.sh: openssl -> openssl ecparam -name prime256v1 -noout -genkey -out ./domain/domain.key
String[] commands = {
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
OPENSSL +
" ecparam -name prime256v1 -noout -genkey -out " + CERT_KEY_PATH
};
exec_(commands);
if (domainKey.exists() && domainKey.length() != 0) {
System.out.println("domainKey创建成功 " + domainKey.getAbsolutePath());
// CERT_KEY_PATH = domainKey.getAbsolutePath();
} else {
System.out.println("domainKey创建失败");
System.exit(1);
}
}
/**
* 在申请证书前保存域名相关信息
*/
public static void onBeforeIssue() {
if (new File(DOMAIN_CONF_PATH).length() > 0) {
System.out.println("使用已有domain.conf: "+ DOMAIN_CONF_PATH);
return;
}
String Le_Domain = MAIN_DOMAIN;
String Le_Alt = ALT_DOMAIN;
String Le_Webroot = "dns";
String Le_API = CA_ZEROSSL;
String Le_Keylength = DEFAULT_DOMAIN_KEY_LENGTH;
saveDomainConf("Le_Domain", Le_Domain);
saveDomainConf("Le_Alt", Le_Alt);
saveDomainConf("Le_Webroot", Le_Webroot);
saveDomainConf("Le_API", Le_API);
saveDomainConf("Le_Keylength", Le_Keylength);
}
/**
* 开始申请证书
*
* @throws Exception
*/
public static void sendNewOrder() throws Exception {
String protected_ = String.format(
"{\"nonce\": \"%s\", \"url\": \"%s\", \"alg\": \"ES%s\", \"kid\": %s}",
newNonce(), NEW_ORDER, ECC_KEY_LEN, readConf("ACCOUNT_URL", CA_CONF) // ACCOUNT_URL 为公共变量
);
String protected64 = urlReplace(Base64.getEncoder().encodeToString(protected_.getBytes()));
// {"identifiers": [{"type":"dns","value":"*.pangu.asia"},{"type":"dns","value":"pangu.asia"}] 源码中使用了 _idn()
// _identifiers="$_identifiers,{\"type\":\"$(_getIdType "$d")\",\"value\":\"$(_idn "$d")\"}"
// '{"identifiers": [{"type":"dns","value":"*.pangu.asia"},{"type":"dns","value":"pangu.asia"}]}'
String payload =
String.format("{\"identifiers\": [{\"type\":\"%s\",\"value\":\"%s\"},{\"type\":\"%s\",\"value\":\"%s\"}]}",
DEFAULT_TYPE, MAIN_DOMAIN, DEFAULT_TYPE, ALT_DOMAIN
);
String payload64 = urlReplace(Base64.getEncoder().encodeToString(payload.getBytes()));
String ecSig = sign(protected64 + "." + payload64, ACCOUNT_KEY, "sha256");
System.out.println("ecSig_: " + ecSig);
String sig = urlReplace(hexStrBase64(ecSig));
String body =
String.format("{\"protected\": \"%s\", \"payload\": \"%s\", \"signature\": \"%s\"}",
protected64, payload64, sig
);
System.out.println("body: " + body);
// TODO curl -k -i --raw -X POST --data-raw body -H "user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)" -H "accept: */*" -H "content-type: application/jose+json" -H "host: acme.zerossl.com" "https://acme.zerossl.com/v2/DV90/newOrder"
String[] command = new String[]{
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
CURL +
" -k -i --raw -X POST --data-raw \"" + body.replace("\"", "\\\"") + "\"" +
" -H \"user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)\"" +
" -H \"accept: */*\" -H \"content-type: application/jose+json\"" +
" -H \"host: acme.zerossl.com\"" +
" \"" + NEW_ORDER + "\""
};
String response = exec_(command);
System.out.println("newOrderResponse: " + response);
// domain.conf
String Le_OrderFinalize = valueFor("finalize", response);
saveDomainConf("Le_OrderFinalize", Le_OrderFinalize);
String Le_LinkOrder = valueFor("location",":", response);
saveDomainConf("Le_LinkOrder", Le_LinkOrder);
// authrsForDomain 从返回值中获取authorizations
String[] authrs = Arrays.stream((response.split("authorizations"))[1]
.split("\\]")[0]
.split("\\[")[1]
.split(","))
.map(s -> s.replace("\"", ""))
.toArray(String[]::new);
String[] domains = Arrays.stream((response.split("identifiers"))[1]
.split("\\]")[0]
.split("\\[")[1]
.split("\\}\\s*,\\s*\\{"))
.map(s -> Acme2J.valueFor("value", s))
.toArray(String[]::new);
for (int i = 0; i < domains.length; i++) {
authrsForDomain.put(domains[i], authrs[i]);
System.out.println(domains[i] + ": " + authrs[i]);
}
// nonce
SHARED_NONCE = valueFor("replay-nonce", ":", response);
System.out.println("SHARED_NONCE: " + SHARED_NONCE);
}
/**
* 获取授权 authz
* @throws Exception
*/
public static void getEachAuthorizations() throws Exception {
String Le_Vlist = "";
// {"nonce": "Pzm14CPDcghDivjV8nIZ4toFXQIRl721oDPLA2yCz5I", "url": "https://acme.zerossl.com/v2/DV90/authz/PDl******DPYfyw", "alg": "ES256", "kid": "https://acme.zerossl.com/v2/DV90/account/7pFoW*****reJrg"}
for (String domain : authrsForDomain.keySet()) {
// TODO curl -k -i --raw -X POST -H "user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)" -H "accept: */*" -H "content-type: application/jose+json" -H "host: acme.zerossl.com" "https://acme.zerossl.com/v2/DV90/authz/PDlgUn****DPYfyw"
String url = authrsForDomain.get(domain);
String protected_ =
String.format(
"{\"nonce\": \"%s\", \"url\": \"%s\", \"alg\": \"ES%s\", \"kid\": \"%s\"}",
newNonce(), url, ECC_KEY_LEN, readConf("ACCOUNT_URL", CA_CONF)
);
String protected64 = urlReplace(Base64.getEncoder().encodeToString(protected_.getBytes()));
String payload64 = "";
String ecSig = sign(protected64 + "." + payload64, ACCOUNT_KEY, "sha256");
System.out.println("ecSig_: " + ecSig);
String sig = urlReplace(hexStrBase64(ecSig));
String body =
String.format("{\"protected\": \"%s\", \"payload\": \"%s\", \"signature\": \"%s\"}",
protected64, payload64, sig
);
System.out.println("body: " + body);
// TODO curl -k -i --raw -X POST --data-raw body -H "user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)" -H "accept: */*" -H "content-type: application/jose+json" -H "host: acme.zerossl.com" "https://acme.zerossl.com/v2/DV90/authz/JM******PFRU8KA"
String[] command = new String[]{
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
CURL +
" -k -i --raw -X POST --data-raw \"" + body.replace("\"", "\\\"") + "\"" +
" -H \"user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)\"" +
" -H \"accept: */*\" -H \"content-type: application/jose+json\"" +
" -H \"host: acme.zerossl.com\"" +
" \"" + url + "\""
};
String response = exec_(command);
// status 默认为 pending(待定),不考虑其他情况
// nonce
SHARED_NONCE = valueFor("replay-nonce", ":", response);
// KEY_AUTHORIZATION
String token = getToken(response);
String KEY_AUTHORIZATION = token + "." + calcAccountThumbprint(JWK); // token+"."+thumbprint
// from acme.sh
// dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot$sep$_authz_url"
// _debug dvlist "$dvlist"
// vlist="$vlist$dvlist$dvsep"
Le_Vlist +=
String.format("%s#%s#%s#%s#%s#%s,",
domain, KEY_AUTHORIZATION,getChallUlr(response),"dns-01","dns",url
);
// TODO acme.sh: openssl -> echo -n $KEY_AUTHORIZATION | openssl dgst -sha256 -binary | openssl base64 -e -A
command = new String[]{
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
windows() ?
String.format("set /p=\"%s\" < %s | %s dgst -sha256 -binary | %s base64 -e -A",
KEY_AUTHORIZATION, OPENSSL, OPENSSL, OPENSSL
) :
String.format("echo -n '%s' | %s dgst -sha256 -binary | %s base64 -e -A",
KEY_AUTHORIZATION, OPENSSL, OPENSSL
)
};
KEY_AUTHORIZATION = urlReplace(exec_(command));
// _acme-challenge.domain
txtForDomain.put(domain, KEY_AUTHORIZATION);
}
saveDomainConf("Le_Vlist",Le_Vlist);
// 显示 txt 记录值
System.out.println("#############################################################\n" +
"# 请添加以下txt解析记录\n");
for (String domain : txtForDomain.keySet()) {
System.out.println(
"# "+ "_acme-challenge." + domain.replace("*.", "")+": "+ txtForDomain.get(domain)
);
}
System.out.println("#############################################################");
}
/**
* 验证 dns 记录,challenge & authz
* @param domainConfPath
* @throws Exception
*/
public static void continueVerify(String domainConfPath) throws Exception {
System.out.println(">> >> >> continueVerify");
// 1 从 domain.conf 文件中获取 challenges authrs 信息
String vlist = readConf("Le_Vlist", new File(domainConfPath).getAbsolutePath()); // DOMAIN_CONF
String[] domainInfos = vlist.split(",");
for (String s : domainInfos) {
String[] infos = s.split("#");
authrsForDomain.put(infos[0], infos[5]);
challsForDomain.put(infos[0], infos[2]);
}
MAIN_DOMAIN = domainInfos[0].split("#")[0];
ALT_DOMAIN = domainInfos[0].split("#")[1];
System.out.println(authrsForDomain.toString());
System.out.println(challsForDomain.toString());
// 2 challenge 验证
// TODO curl -k -i --raw -X POST -H "user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)" -H "accept: */*" -H "content-type: application/jose+json" -H "host: acme.zerossl.com" "https://acme.zerossl.com/v2/DV90/chall/vDz******gmsw"
for (String domain : challsForDomain.keySet()) {
System.out.println(">> >> >> >> challenge");
String challUrl = challsForDomain.get(domain);
String protected_ =
String.format(
"{\"nonce\": \"%s\", \"url\": \"%s\", \"alg\": \"ES%s\", \"kid\": \"%s\"}",
newNonce(), challUrl, ECC_KEY_LEN, readConf("ACCOUNT_URL", CA_CONF)
);
String protected64 = urlReplace(Base64.getEncoder().encodeToString(protected_.getBytes()));
String payload64 = urlReplace(Base64.getEncoder().encodeToString("{}".getBytes()));
String ecSig = sign(protected64 + "." + payload64, ACCOUNT_KEY, "sha256");
System.out.println("ecSig_: " + ecSig);
String sig = urlReplace(hexStrBase64(ecSig));
String body =
String.format("{\"protected\": \"%s\", \"payload\": \"%s\", \"signature\": \"%s\"}",
protected64, payload64, sig
);
System.out.println("body: " + body);
// TODO curl -k -i --raw -X POST --data-raw body -H "user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)" -H "accept: */*" -H "content-type: application/jose+json" -H "host: acme.zerossl.com" "https://acme.zerossl.com/v2/DV90/chall/ZZ******q189Q"
String[] command = new String[]{
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
CURL +
" -k -i --raw -X POST --data-raw \"" + body.replace("\"", "\\\"") + "\"" +
" -H \"user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)\"" +
" -H \"accept: */*\" -H \"content-type: application/jose+json\"" +
" -H \"host: acme.zerossl.com\"" +
" \"" + challUrl + "\""
};
String response = exec_(command);
// nonce
SHARED_NONCE = valueFor("replay-nonce", ":", response);
// status
String status = valueFor("status", response);
// status: processing, invalid
if (status.contains("invalid")) {
System.out.println("出现错误,验证失败,详细信息请查看日志: "+response);
System.exit(1);
} else if (status.contains("valid")) {
System.out.println("验证成功,开始获取证书");
return;
} else if (status.contains("processing")) {
// 3 验证 dns 记录
// sendAuthrs 直到dns-txt记录验证成功
do {
System.out.println(">> >> >> >> >> 验证域名dns记录");
String authUrl = authrsForDomain.get(domain);
protected_ =
String.format(
"{\"nonce\": \"%s\", \"url\": \"%s\", \"alg\": \"ES%s\", \"kid\": \"%s\"}",
newNonce(), authUrl, ECC_KEY_LEN, readConf("ACCOUNT_URL", CA_CONF)
);
protected64 = urlReplace(Base64.getEncoder().encodeToString(protected_.getBytes()));
payload64 = "";
ecSig = sign(protected64 + "." + payload64, ACCOUNT_KEY, "sha256");
System.out.println("ecSig_: " + ecSig);
sig = urlReplace(hexStrBase64(ecSig));
body =
String.format("{\"protected\": \"%s\", \"payload\": \"%s\", \"signature\": \"%s\"}",
protected64, payload64, sig
);
System.out.println("body: " + body);
// TODO curl -k -i --raw -X POST --data-raw body -H "user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)" -H "accept: */*" -H "content-type: application/jose+json" -H "host: acme.zerossl.com" "https://acme.zerossl.com/v2/DV90/authz/JMkn*******8KA"
command = new String[]{
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
CURL +
" -k -i --raw -X POST --data-raw \"" + body.replace("\"", "\\\"") + "\"" +
" -H \"user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)\"" +
" -H \"accept: */*\" -H \"content-type: application/jose+json\"" +
" -H \"host: acme.zerossl.com\"" +
" \"" + authUrl + "\""
};
response = exec_(command);
// nonce
SHARED_NONCE = valueFor("replay-nonce", ":", response);
// status: pending, valid
status = valueFor("status", response);
if (status.contains("invalid")) {
System.out.println("出错了!");
}
System.out.println("等待十秒 wait 10s");
Thread.sleep(10000);
// 4 dns 验证通过 开始下一条 domain 的 challenge 验证
} while (!status.contains("valid"));
} else {
System.out.println("未知状态,验证失败,详细信息请查看日志: " + response);
}
}
System.out.println("验证成功,开始获取证书");
}
/**
* 创建 domain.crt 文件
* @param csrPath
* @param csrConfPath
* @throws Exception
*/
public static void createCrt(String csrConfPath, String csrPath) throws Exception {
// 1 创建 domain.csr.conf
File csrConf = new File(csrConfPath);
if (csrConf.length() > 0) {
System.out.println("使用已有 domain.csr ");
return;
}
csrConf.mkdirs();
csrConf.delete();
csrConf.createNewFile();
// subjectAltName=DNS:b.moodle.net.cn,DNS:*.b.moodle.net.cn
String subjectAltName = "\nsubjectAltName=";
for (String domain : authrsForDomain.keySet()) {
subjectAltName += ("DNS:" + domain + ",");
}
subjectAltName = subjectAltName.substring(0, subjectAltName.length() - 1);
try (FileWriter writer = new FileWriter(csrConf)) {
writer.write(
"[ req_distinguished_name ]\n[ req ]\n" +
"distinguished_name = req_distinguished_name\n" +
"req_extensions = v3_req\n" +
"[ v3_req ]\n" +
"extendedKeyUsage=serverAuth,clientAuth\n" +
subjectAltName
);
System.out.println("写入domain.csr.conf文件成功");
} catch (Exception e) {
System.out.println("写入domain.csr.conf文件失败");
e.printStackTrace();
System.exit(1);
}
// 2 创建 domain.csr
// TODO acme.sh: openssl req -new -sha256 -key "$csrkey" -subj "/CN=$_csr_cn" -config "$csrconf" -out "$csr" # _csr_cn="$(_idn "$domain")"
String[] command = {
windows() ? "cmd" : "/bin/sh",
windows() ? "/c" : "-c",
OPENSSL + String.format(" req -new -sha256 -key \"%s\" -subj \"/CN=%s\" -config \"%s\" -out \"%s\"",
CERT_KEY_PATH,MAIN_DOMAIN,csrConfPath,csrPath
)
};
String result = exec_(command);
System.out.println(result);
if (new File(csrPath).exists() && (new File(csrPath).length() > 0)) {
System.out.println("domain.csr创建成功");
}
}
/**
* 完成 orderFinalize & order 获取并保存 certificate 链接
* @param doaminConfPath
* @param csrPath
* @throws Exception
*/
public static void finalizeOrder(String doaminConfPath,String csrPath) throws Exception {
String orderFinalizeUrl = readConf("Le_OrderFinalize",doaminConfPath);
String linkOrder = readConf("Le_LinkOrder", doaminConfPath);
// 1 send order finalize
// TODO curl -k -i --raw -X POST -H "user-agent: acme.sh/3.0.8 (https://github.com/acmesh-official/acme.sh)" -H "accept: */*" -H "content-type: application/jose+json" -H "host: acme.zerossl.com" "https://acme.zerossl.com/v2/DV90/order/pI3*****LNTz0A/finalize"
String protected_ =
String.format(
"{\"nonce\": \"%s\", \"url\": \"%s\", \"alg\": \"ES%s\", \"kid\": \"%s\"}",
newNonce(), orderFinalizeUrl, ECC_KEY_LEN, readConf("ACCOUNT_URL", CA_CONF)
);
String protected64 = urlReplace(Base64.getEncoder().encodeToString(protected_.getBytes()));
// der="$(_getfile "${CSR_PATH}" "${BEGIN_CSR}" "${END_CSR}" | tr -d "\r\n" | _url_replace)" if ! _send_signed_request "${Le_OrderFinalize}" "{\"csr\": \"$der\"}"; then
String der = urlReplace(new String(Files.readAllBytes(Paths.get(csrPath)), "utf-8").replaceAll("-----.*?-----", "").replaceAll("[\r\n]", ""));