]> git.proxmox.com Git - proxmox-acme.git/blob - src/proxmox-acme
a00d23a2a120e39e05c93837780e490eb2c5e916
[proxmox-acme.git] / src / proxmox-acme
1 #!/bin/bash
2
3 VER=1.0
4
5 PROJECT_NAME="ProxmoxACME"
6
7 USER_AGENT="$PROJECT_NAME/$VER"
8
9 DNS_PLUGIN_PATH="/usr/share/proxmox-acme/dnsapi"
10 HTTP_HEADER="$(mktemp)"
11
12 DEBUG="0"
13
14 _base64() {
15 openssl base64 -e | tr -d '\r\n'
16 }
17
18 _dbase64() {
19 openssl base64 -d
20 }
21
22 # Usage: hashalg [outputhex]
23 # Output Base64-encoded digest
24 _digest() {
25 alg="$1"
26
27 if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
28 if [ "$2" ]; then
29 openssl dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' '
30 else
31 openssl dgst -"$alg" -binary | _base64
32 fi
33 fi
34 }
35
36 _usage() {
37 __red "$@" >&2
38 printf "\n" >&2
39 }
40
41 _upper_case() {
42 # shellcheck disable=SC2018,SC2019
43 tr 'a-z' 'A-Z'
44 }
45
46 _lower_case() {
47 # shellcheck disable=SC2018,SC2019
48 tr 'A-Z' 'a-z'
49 }
50
51 _startswith() {
52 _str="$1"
53 _sub="$2"
54 echo "$_str" | grep "^$_sub" >/dev/null 2>&1
55 }
56
57 _endswith() {
58 _str="$1"
59 _sub="$2"
60 echo "$_str" | grep -- "$_sub\$" >/dev/null 2>&1
61 }
62
63 _contains() {
64 _str="$1"
65 _sub="$2"
66 echo "$_str" | grep -- "$_sub" >/dev/null 2>&1
67 }
68
69 # str index [sep]
70 _getfield() {
71 _str="$1"
72 _findex="$2"
73 _sep="$3"
74
75 if [ -z "$_sep" ]; then
76 _sep=","
77 fi
78
79 _ffi="$_findex"
80 while [ "$_ffi" -gt "0" ]; do
81 _fv="$(echo "$_str" | cut -d "$_sep" -f "$_ffi")"
82 if [ "$_fv" ]; then
83 printf -- "%s" "$_fv"
84 return 0
85 fi
86 _ffi="$(_math "$_ffi" - 1)"
87 done
88
89 printf -- "%s" "$_str"
90
91 }
92
93 _exists() {
94 cmd="$1"
95 if eval type type >/dev/null 2>&1; then
96 type "$cmd" >/dev/null 2>&1
97 else command
98 command -v "$cmd" >/dev/null 2>&1
99 fi
100 ret="$?"
101 return $ret
102 }
103
104 # a + b
105 _math() {
106 _m_opts="$@"
107 printf "%s" "$(($_m_opts))"
108 }
109
110 _egrep_o() {
111 if ! egrep -o "$1" 2>/dev/null; then
112 sed -n 's/.*\('"$1"'\).*/\1/p'
113 fi
114 }
115
116 _h2b() {
117 if _exists xxd; then
118 if _contains "$(xxd --help 2>&1)" "assumes -c30"; then
119 if xxd -r -p -c 9999 2>/dev/null; then
120 return
121 fi
122 else
123 if xxd -r -p 2>/dev/null; then
124 return
125 fi
126 fi
127 fi
128
129 hex=$(cat)
130 ic=""
131 jc=""
132 _debug2 _URGLY_PRINTF "$_URGLY_PRINTF"
133 if [ -z "$_URGLY_PRINTF" ]; then
134 if [ "$_ESCAPE_XARGS" ] && _exists xargs; then
135 _debug2 "xargs"
136 echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/g' | xargs printf
137 else
138 for h in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/ \1/g'); do
139 if [ -z "$h" ]; then
140 break
141 fi
142 printf "\x$h%s"
143 done
144 fi
145 else
146 for c in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\)/ \1/g'); do
147 if [ -z "$ic" ]; then
148 ic=$c
149 continue
150 fi
151 jc=$c
152 ic="$(_h_char_2_dec "$ic")"
153 jc="$(_h_char_2_dec "$jc")"
154 printf '\'"$(printf "%o" "$(_math "$ic" \* 16 + $jc)")""%s"
155 ic=""
156 jc=""
157 done
158 fi
159
160 }
161
162 #Usage: keyfile hashalg
163 #Output: Base64-encoded signature value
164 _sign() {
165 keyfile="$1"
166 alg="$2"
167 if [ -z "$alg" ]; then
168 _usage "Usage: _sign keyfile hashalg"
169 return 1
170 fi
171
172 _sign_openssl="${ACME_OPENSSL_BIN:-openssl} dgst -sign $keyfile "
173
174 if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1 || grep "BEGIN PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then
175 $_sign_openssl -$alg | _base64
176 elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then
177 if ! _signedECText="$($_sign_openssl -sha$__ECC_KEY_LEN | ${ACME_OPENSSL_BIN:-openssl} asn1parse -inform DER)"; then
178 _err "Sign failed: $_sign_openssl"
179 _err "Key file: $keyfile"
180 _err "Key content:$(wc -l <"$keyfile") lines"
181 return 1
182 fi
183 _debug3 "_signedECText" "$_signedECText"
184 _ec_r="$(echo "$_signedECText" | _head_n 2 | _tail_n 1 | cut -d : -f 4 | tr -d "\r\n")"
185 _ec_s="$(echo "$_signedECText" | _head_n 3 | _tail_n 1 | cut -d : -f 4 | tr -d "\r\n")"
186 if [ "$__ECC_KEY_LEN" -eq "256" ]; then
187 while [ "${#_ec_r}" -lt "64" ]; do
188 _ec_r="0${_ec_r}"
189 done
190 while [ "${#_ec_s}" -lt "64" ]; do
191 _ec_s="0${_ec_s}"
192 done
193 fi
194 if [ "$__ECC_KEY_LEN" -eq "384" ]; then
195 while [ "${#_ec_r}" -lt "96" ]; do
196 _ec_r="0${_ec_r}"
197 done
198 while [ "${#_ec_s}" -lt "96" ]; do
199 _ec_s="0${_ec_s}"
200 done
201 fi
202 if [ "$__ECC_KEY_LEN" -eq "512" ]; then
203 while [ "${#_ec_r}" -lt "132" ]; do
204 _ec_r="0${_ec_r}"
205 done
206 while [ "${#_ec_s}" -lt "132" ]; do
207 _ec_s="0${_ec_s}"
208 done
209 fi
210 _debug3 "_ec_r" "$_ec_r"
211 _debug3 "_ec_s" "$_ec_s"
212 printf "%s" "$_ec_r$_ec_s" | _h2b | _base64
213 else
214 _err "Unknown key file format."
215 return 1
216 fi
217
218 }
219
220 #dummy function because proxmox-acme does not call inithttp
221 _resethttp() {
222 :
223 }
224
225 _HTTP_MAX_RETRY=8
226
227 # body url [needbase64] [POST|PUT|DELETE] [ContentType]
228 _post() {
229 body="$1"
230 _post_url="$2"
231 needbase64="$3"
232 httpmethod="$4"
233 _postContentType="$5"
234 _sleep_retry_sec=1
235 _http_retry_times=0
236 _hcode=0
237 while [ "${_http_retry_times}" -le "$_HTTP_MAX_RETRY" ]; do
238 [ "$_http_retry_times" = "$_HTTP_MAX_RETRY" ]
239 _lastHCode="$?"
240 _debug "Retrying post"
241 _post_impl "$body" "$_post_url" "$needbase64" "$httpmethod" "$_postContentType" "$_lastHCode"
242 _hcode="$?"
243 _debug _hcode "$_hcode"
244 if [ "$_hcode" = "0" ]; then
245 break
246 fi
247 _http_retry_times=$(_math $_http_retry_times + 1)
248 _sleep $_sleep_retry_sec
249 done
250 return $_hcode
251 }
252
253 # body url [needbase64] [POST|PUT|DELETE] [ContentType] [displayError]
254 _post_impl() {
255 body="$1"
256 _post_url="$2"
257 needbase64="$3"
258 httpmethod="$4"
259 _postContentType="$5"
260 displayError="$6"
261
262 if [ -z "$httpmethod" ]; then
263 httpmethod="POST"
264 fi
265
266 _CURL="curl -L --silent --dump-header $HTTP_HEADER -g "
267 if [ "$HTTPS_INSECURE" ]; then
268 _CURL="$_CURL --insecure "
269 fi
270 if [ "$httpmethod" = "HEAD" ]; then
271 _CURL="$_CURL -I "
272 fi
273 if [ "$needbase64" ]; then
274 if [ "$body" ]; then
275 if [ "$_postContentType" ]; then
276 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)"
277 else
278 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)"
279 fi
280 else
281 if [ "$_postContentType" ]; then
282 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url" | _base64)"
283 else
284 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url" | _base64)"
285 fi
286 fi
287 else
288 if [ "$body" ]; then
289 if [ "$_postContentType" ]; then
290 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")"
291 else
292 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")"
293 fi
294 else
295 if [ "$_postContentType" ]; then
296 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url")"
297 else
298 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url")"
299 fi
300 fi
301 fi
302 _ret="$?"
303 if [ "$_ret" != "0" ]; then
304 if [ -z "$displayError" ] || [ "$displayError" = "0" ]; then
305 _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret"
306 fi
307 fi
308 printf "%s" "$response"
309 return $_ret
310 }
311
312 # url getheader timeout
313 _get() {
314 url="$1"
315 onlyheader="$2"
316 t="$3"
317 _sleep_retry_sec=1
318 _http_retry_times=0
319 _hcode=0
320 while [ "${_http_retry_times}" -le "$_HTTP_MAX_RETRY" ]; do
321 [ "$_http_retry_times" = "$_HTTP_MAX_RETRY" ]
322 _lastHCode="$?"
323 _debug "Retrying GET"
324 _get_impl "$url" "$onlyheader" "$t" "$_lastHCode"
325 _hcode="$?"
326 _debug _hcode "$_hcode"
327 if [ "$_hcode" = "0" ]; then
328 break
329 fi
330 _http_retry_times=$(_math $_http_retry_times + 1)
331 _sleep $_sleep_retry_sec
332 done
333 return $_hcode
334 }
335
336 # url getheader timeout displayError
337 _get_impl() {
338 url="$1"
339 onlyheader="$2"
340 t="$3"
341 displayError="$4"
342
343 _CURL="curl -L --silent --dump-header $HTTP_HEADER -g "
344 if [ "$HTTPS_INSECURE" ]; then
345 _CURL="$_CURL --insecure "
346 fi
347 if [ "$t" ]; then
348 _CURL="$_CURL --connect-timeout $t"
349 fi
350 if [ "$onlyheader" ]; then
351 $_CURL -I --user-agent "USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url"
352 else
353 $_CURL --user-agent "USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url"
354 fi
355 ret=$?
356 if [ "$ret" != "0" ]; then
357 if [ -z "$displayError" ] || [ "$displayError" = "0" ]; then
358 _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $ret"
359 fi
360 fi
361 return $ret
362 }
363
364 _head_n() {
365 head -n "$1"
366 }
367
368 _tail_n() {
369 tail -n "$1"
370 }
371
372 # stdin output hexstr splited by one space
373 # input:"abc"
374 # output: " 61 62 63"
375 _hex_dump() {
376 od -A n -v -t x1 | tr -s " " | sed 's/ $//' | tr -d "\r\t\n"
377 }
378
379 # stdin stdout
380 _url_encode() {
381 _hex_str=$(_hex_dump)
382 for _hex_code in $_hex_str; do
383 #upper case
384 case "${_hex_code}" in
385 "41")
386 printf "%s" "A"
387 ;;
388 "42")
389 printf "%s" "B"
390 ;;
391 "43")
392 printf "%s" "C"
393 ;;
394 "44")
395 printf "%s" "D"
396 ;;
397 "45")
398 printf "%s" "E"
399 ;;
400 "46")
401 printf "%s" "F"
402 ;;
403 "47")
404 printf "%s" "G"
405 ;;
406 "48")
407 printf "%s" "H"
408 ;;
409 "49")
410 printf "%s" "I"
411 ;;
412 "4a")
413 printf "%s" "J"
414 ;;
415 "4b")
416 printf "%s" "K"
417 ;;
418 "4c")
419 printf "%s" "L"
420 ;;
421 "4d")
422 printf "%s" "M"
423 ;;
424 "4e")
425 printf "%s" "N"
426 ;;
427 "4f")
428 printf "%s" "O"
429 ;;
430 "50")
431 printf "%s" "P"
432 ;;
433 "51")
434 printf "%s" "Q"
435 ;;
436 "52")
437 printf "%s" "R"
438 ;;
439 "53")
440 printf "%s" "S"
441 ;;
442 "54")
443 printf "%s" "T"
444 ;;
445 "55")
446 printf "%s" "U"
447 ;;
448 "56")
449 printf "%s" "V"
450 ;;
451 "57")
452 printf "%s" "W"
453 ;;
454 "58")
455 printf "%s" "X"
456 ;;
457 "59")
458 printf "%s" "Y"
459 ;;
460 "5a")
461 printf "%s" "Z"
462 ;;
463
464 #lower case
465 "61")
466 printf "%s" "a"
467 ;;
468 "62")
469 printf "%s" "b"
470 ;;
471 "63")
472 printf "%s" "c"
473 ;;
474 "64")
475 printf "%s" "d"
476 ;;
477 "65")
478 printf "%s" "e"
479 ;;
480 "66")
481 printf "%s" "f"
482 ;;
483 "67")
484 printf "%s" "g"
485 ;;
486 "68")
487 printf "%s" "h"
488 ;;
489 "69")
490 printf "%s" "i"
491 ;;
492 "6a")
493 printf "%s" "j"
494 ;;
495 "6b")
496 printf "%s" "k"
497 ;;
498 "6c")
499 printf "%s" "l"
500 ;;
501 "6d")
502 printf "%s" "m"
503 ;;
504 "6e")
505 printf "%s" "n"
506 ;;
507 "6f")
508 printf "%s" "o"
509 ;;
510 "70")
511 printf "%s" "p"
512 ;;
513 "71")
514 printf "%s" "q"
515 ;;
516 "72")
517 printf "%s" "r"
518 ;;
519 "73")
520 printf "%s" "s"
521 ;;
522 "74")
523 printf "%s" "t"
524 ;;
525 "75")
526 printf "%s" "u"
527 ;;
528 "76")
529 printf "%s" "v"
530 ;;
531 "77")
532 printf "%s" "w"
533 ;;
534 "78")
535 printf "%s" "x"
536 ;;
537 "79")
538 printf "%s" "y"
539 ;;
540 "7a")
541 printf "%s" "z"
542 ;;
543
544 #numbers
545 "30")
546 printf "%s" "0"
547 ;;
548 "31")
549 printf "%s" "1"
550 ;;
551 "32")
552 printf "%s" "2"
553 ;;
554 "33")
555 printf "%s" "3"
556 ;;
557 "34")
558 printf "%s" "4"
559 ;;
560 "35")
561 printf "%s" "5"
562 ;;
563 "36")
564 printf "%s" "6"
565 ;;
566 "37")
567 printf "%s" "7"
568 ;;
569 "38")
570 printf "%s" "8"
571 ;;
572 "39")
573 printf "%s" "9"
574 ;;
575 "2d")
576 printf "%s" "-"
577 ;;
578 "5f")
579 printf "%s" "_"
580 ;;
581 "2e")
582 printf "%s" "."
583 ;;
584 "7e")
585 printf "%s" "~"
586 ;;
587
588 #other hex
589 *)
590 printf '%%%s' "$_hex_code"
591 ;;
592 esac
593 done
594 }
595
596 # Usage: hashalg secret_hex [outputhex]
597 # Output binary hmac
598 _hmac() {
599 alg="$1"
600 secret_hex="$2"
601 outputhex="$3"
602
603 if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ]; then
604 if [ "$outputhex" ]; then
605 (openssl dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" 2>/dev/null || openssl dgst -"$alg" -hmac "$(printf "%s" "$secret_hex" | _h2b)") | cut -d = -f 2 | tr -d ' '
606 else
607 openssl dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" -binary 2>/dev/null || openssl dgst -"$alg" -hmac "$(printf "%s" "$secret_hex" | _h2b)" -binary
608 fi
609 fi
610 }
611
612 # domain
613 _is_idn() {
614 _is_idn_d="$1"
615 _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '0-9' | tr -d 'a-z' | tr -d 'A-Z' | tr -d '*.,-_')
616 [ "$_idn_temp" ]
617 }
618
619 # aa.com
620 _idn() {
621 __idn_d="$1"
622 if ! _is_idn "$__idn_d"; then
623 printf "%s" "$__idn_d"
624 return 0
625 fi
626
627 if _exists idn; then
628 idn "$__idn_d" | tr -d "\r\n"
629 else
630 _err "Please install idn to process IDN names."
631 fi
632 }
633
634 _normalizeJson() {
635 sed "s/\" *: *\([\"{\[]\)/\":\1/g" | sed "s/^ *\([^ ]\)/\1/" | tr -d "\r\n"
636 }
637
638 # options file
639 _sed_i() {
640 sed -i "$1" "$2"
641 }
642
643 # sleep sec
644 _sleep() {
645 sleep "$1"
646 }
647
648 _stat() {
649 stat -c '%U:%G' "$1" 2>/dev/null
650 }
651
652 _time() {
653 date -u "+%s"
654 }
655
656 _utc_date() {
657 date -u "+%Y-%m-%d %H:%M:%S"
658 }
659
660 # stubbed/aliased:
661 __green() {
662 printf -- "%b" "$1"
663 }
664
665 __red() {
666 printf -- "%b" "$1"
667 }
668
669 _log() {
670 return 0
671 }
672
673 _info() {
674 printf -- "%s" "[$(date)] " >&1
675 echo "$1"
676 }
677
678 _err() {
679 printf -- "%s" "[$(date)] " >&2
680 if [ -z "$2" ]; then
681 __red "$1" >&2
682 else
683 __red "$1='$2'" >&2
684 fi
685 printf "\n" >&2
686 return 1
687 }
688
689 # key
690 _readaccountconf() {
691 echo "${!1}"
692 }
693
694 # key
695 _readaccountconf_mutable() {
696 _readaccountconf "$1"
697 }
698
699 # no-ops:
700 _clearaccountconf() {
701 return 0
702 }
703
704 _cleardomainconf() {
705 return 0
706 }
707
708 _debug() {
709 if [[ $DEBUG -eq 0 ]]; then
710 return
711 fi
712 printf -- "%s" "[$(date)] " >&1
713 echo "$1 $2"
714 }
715
716 _debug2() {
717 _debug $1 $2
718 }
719
720 _debug3() {
721 _debug $1 $2
722 }
723
724 _secure_debug() {
725 _debug $1 $2
726 }
727
728 _secure_debug2() {
729 _debug $1 $2
730 }
731
732 _secure_debug3() {
733 _debug $1 $2
734 }
735
736 _saveaccountconf() {
737 return 0
738 }
739
740 _saveaccountconf_mutable() {
741 return 0
742 }
743
744 _save_conf() {
745 return 0
746 }
747
748 _savedomainconf() {
749 return 0
750 }
751
752 _source_plugin_config() {
753 return 0
754 }
755
756 # Proxmox implementation to inject the DNSAPI variables
757 _load_plugin_config() {
758 while IFS= read -r line; do
759 ADDR=(${line/=/ })
760 key="${ADDR[0]}"
761 value="${ADDR[1]}"
762
763 # acme.sh uses eval insted of export
764 if [ -n "$key" ]; then
765 export "$key"="$value"
766 fi
767 done
768 }
769
770 # call setup and teardown direct
771 # the parameter must be set in the correct order
772 # $1 <String> DNS Plugin name
773 # $2 <String> Fully Qualified Domain Name
774 # $3 <String> value for TXT record
775 # $4 <String> DNS plugin auth and config parameter separated by ","
776 # $5 <Integer> 0 is off, and the default all others are on.
777
778 setup() {
779 dns_plugin="dns_$1"
780 dns_plugin_path="${DNS_PLUGIN_PATH}/${dns_plugin}.sh"
781 fqdn="_acme-challenge.$2"
782 DEBUG=$3
783 IFS= read -r txtvalue
784 plugin_conf_string=$4
785
786 _load_plugin_config
787
788 if ! . "$dns_plugin_path"; then
789 _err "Load file $dns_plugin error."
790 return 1
791 fi
792
793 addcommand="${dns_plugin}_add"
794 if ! _exists "$addcommand"; then
795 _err "It seems that your api file is not correct, it must have a function named: $addcommand"
796 return 1
797 fi
798
799 if ! $addcommand "$fqdn" "$txtvalue"; then
800 _err "Error add txt for domain:$fulldomain"
801 return 1
802 fi
803 }
804
805 teardown() {
806 dns_plugin="dns_$1"
807 dns_plugin_path="${DNS_PLUGIN_PATH}/${dns_plugin}.sh"
808 fqdn="_acme-challenge.$2"
809 DEBUG=$3
810 IFS= read -r txtvalue
811
812 _load_plugin_config
813
814 if ! . "$dns_plugin_path"; then
815 _err "Load file $dns_plugin error."
816 return 1
817 fi
818
819 rmcommand="${dns_plugin}_rm"
820 if ! _exists "$rmcommand"; then
821 _err "It seems that your api file is not correct, it must have a function named: $rmcommand"
822 return 1
823 fi
824
825 if ! $rmcommand "$fqdn" "$txtvalue"; then
826 _err "Error add txt for domain:$fulldomain"
827 return 1
828 fi
829 }
830
831 "$@"