]> git.proxmox.com Git - proxmox-acme.git/blob - src/proxmox-acme
6cc7b5feba18d62fa9108bfd2d4e883e86d03a4c
[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 # body url [needbase64] [POST|PUT|DELETE] [ContentType]
226 _post() {
227 body="$1"
228 _post_url="$2"
229 needbase64="$3"
230 httpmethod="$4"
231 _postContentType="$5"
232
233 if [ -z "$httpmethod" ]; then
234 httpmethod="POST"
235 fi
236
237 _CURL="curl -L --silent --dump-header $HTTP_HEADER -g "
238 if [ "$HTTPS_INSECURE" ]; then
239 _CURL="$_CURL --insecure "
240 fi
241 if [ "$httpmethod" = "HEAD" ]; then
242 _CURL="$_CURL -I "
243 fi
244 if [ "$needbase64" ]; then
245 if [ "$body" ]; then
246 if [ "$_postContentType" ]; then
247 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)"
248 else
249 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)"
250 fi
251 else
252 if [ "$_postContentType" ]; then
253 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)"
254 else
255 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url" | _base64)"
256 fi
257 fi
258 else
259 if [ "$body" ]; then
260 if [ "$_postContentType" ]; then
261 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")"
262 else
263 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")"
264 fi
265 else
266 if [ "$_postContentType" ]; then
267 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url")"
268 else
269 response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url")"
270 fi
271 fi
272 fi
273 _ret="$?"
274 if [ "$_ret" != "0" ]; then
275 _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret"
276 fi
277 printf "%s" "$response"
278 return $_ret
279 }
280
281 # url getheader timeout
282 _get() {
283 url="$1"
284 onlyheader="$2"
285 t="$3"
286
287 _CURL="curl -L --silent --dump-header $HTTP_HEADER -g "
288 if [ "$HTTPS_INSECURE" ]; then
289 _CURL="$_CURL --insecure "
290 fi
291 if [ "$t" ]; then
292 _CURL="$_CURL --connect-timeout $t"
293 fi
294 if [ "$onlyheader" ]; then
295 $_CURL -I --user-agent "USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url"
296 else
297 $_CURL --user-agent "USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url"
298 fi
299 ret=$?
300 if [ "$ret" != "0" ]; then
301 _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $ret"
302 fi
303 return $ret
304 }
305
306 _head_n() {
307 head -n "$1"
308 }
309
310 _tail_n() {
311 tail -n "$1"
312 }
313
314 # stdin output hexstr splited by one space
315 # input:"abc"
316 # output: " 61 62 63"
317 _hex_dump() {
318 od -A n -v -t x1 | tr -s " " | sed 's/ $//' | tr -d "\r\t\n"
319 }
320
321 # stdin stdout
322 _url_encode() {
323 _hex_str=$(_hex_dump)
324 for _hex_code in $_hex_str; do
325 #upper case
326 case "${_hex_code}" in
327 "41")
328 printf "%s" "A"
329 ;;
330 "42")
331 printf "%s" "B"
332 ;;
333 "43")
334 printf "%s" "C"
335 ;;
336 "44")
337 printf "%s" "D"
338 ;;
339 "45")
340 printf "%s" "E"
341 ;;
342 "46")
343 printf "%s" "F"
344 ;;
345 "47")
346 printf "%s" "G"
347 ;;
348 "48")
349 printf "%s" "H"
350 ;;
351 "49")
352 printf "%s" "I"
353 ;;
354 "4a")
355 printf "%s" "J"
356 ;;
357 "4b")
358 printf "%s" "K"
359 ;;
360 "4c")
361 printf "%s" "L"
362 ;;
363 "4d")
364 printf "%s" "M"
365 ;;
366 "4e")
367 printf "%s" "N"
368 ;;
369 "4f")
370 printf "%s" "O"
371 ;;
372 "50")
373 printf "%s" "P"
374 ;;
375 "51")
376 printf "%s" "Q"
377 ;;
378 "52")
379 printf "%s" "R"
380 ;;
381 "53")
382 printf "%s" "S"
383 ;;
384 "54")
385 printf "%s" "T"
386 ;;
387 "55")
388 printf "%s" "U"
389 ;;
390 "56")
391 printf "%s" "V"
392 ;;
393 "57")
394 printf "%s" "W"
395 ;;
396 "58")
397 printf "%s" "X"
398 ;;
399 "59")
400 printf "%s" "Y"
401 ;;
402 "5a")
403 printf "%s" "Z"
404 ;;
405
406 #lower case
407 "61")
408 printf "%s" "a"
409 ;;
410 "62")
411 printf "%s" "b"
412 ;;
413 "63")
414 printf "%s" "c"
415 ;;
416 "64")
417 printf "%s" "d"
418 ;;
419 "65")
420 printf "%s" "e"
421 ;;
422 "66")
423 printf "%s" "f"
424 ;;
425 "67")
426 printf "%s" "g"
427 ;;
428 "68")
429 printf "%s" "h"
430 ;;
431 "69")
432 printf "%s" "i"
433 ;;
434 "6a")
435 printf "%s" "j"
436 ;;
437 "6b")
438 printf "%s" "k"
439 ;;
440 "6c")
441 printf "%s" "l"
442 ;;
443 "6d")
444 printf "%s" "m"
445 ;;
446 "6e")
447 printf "%s" "n"
448 ;;
449 "6f")
450 printf "%s" "o"
451 ;;
452 "70")
453 printf "%s" "p"
454 ;;
455 "71")
456 printf "%s" "q"
457 ;;
458 "72")
459 printf "%s" "r"
460 ;;
461 "73")
462 printf "%s" "s"
463 ;;
464 "74")
465 printf "%s" "t"
466 ;;
467 "75")
468 printf "%s" "u"
469 ;;
470 "76")
471 printf "%s" "v"
472 ;;
473 "77")
474 printf "%s" "w"
475 ;;
476 "78")
477 printf "%s" "x"
478 ;;
479 "79")
480 printf "%s" "y"
481 ;;
482 "7a")
483 printf "%s" "z"
484 ;;
485
486 #numbers
487 "30")
488 printf "%s" "0"
489 ;;
490 "31")
491 printf "%s" "1"
492 ;;
493 "32")
494 printf "%s" "2"
495 ;;
496 "33")
497 printf "%s" "3"
498 ;;
499 "34")
500 printf "%s" "4"
501 ;;
502 "35")
503 printf "%s" "5"
504 ;;
505 "36")
506 printf "%s" "6"
507 ;;
508 "37")
509 printf "%s" "7"
510 ;;
511 "38")
512 printf "%s" "8"
513 ;;
514 "39")
515 printf "%s" "9"
516 ;;
517 "2d")
518 printf "%s" "-"
519 ;;
520 "5f")
521 printf "%s" "_"
522 ;;
523 "2e")
524 printf "%s" "."
525 ;;
526 "7e")
527 printf "%s" "~"
528 ;;
529
530 #other hex
531 *)
532 printf '%%%s' "$_hex_code"
533 ;;
534 esac
535 done
536 }
537
538 # Usage: hashalg secret_hex [outputhex]
539 # Output binary hmac
540 _hmac() {
541 alg="$1"
542 secret_hex="$2"
543 outputhex="$3"
544
545 if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ]; then
546 if [ "$outputhex" ]; then
547 (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 ' '
548 else
549 openssl dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" -binary 2>/dev/null || openssl dgst -"$alg" -hmac "$(printf "%s" "$secret_hex" | _h2b)" -binary
550 fi
551 fi
552 }
553
554 # domain
555 _is_idn() {
556 _is_idn_d="$1"
557 _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '0-9' | tr -d 'a-z' | tr -d 'A-Z' | tr -d '*.,-_')
558 [ "$_idn_temp" ]
559 }
560
561 # aa.com
562 _idn() {
563 __idn_d="$1"
564 if ! _is_idn "$__idn_d"; then
565 printf "%s" "$__idn_d"
566 return 0
567 fi
568
569 if _exists idn; then
570 idn "$__idn_d" | tr -d "\r\n"
571 else
572 _err "Please install idn to process IDN names."
573 fi
574 }
575
576 _normalizeJson() {
577 sed "s/\" *: *\([\"{\[]\)/\":\1/g" | sed "s/^ *\([^ ]\)/\1/" | tr -d "\r\n"
578 }
579
580 # options file
581 _sed_i() {
582 sed -i "$1" "$2"
583 }
584
585 # sleep sec
586 _sleep() {
587 sleep "$1"
588 }
589
590 _stat() {
591 stat -c '%U:%G' "$1" 2>/dev/null
592 }
593
594 _time() {
595 date -u "+%s"
596 }
597
598 _utc_date() {
599 date -u "+%Y-%m-%d %H:%M:%S"
600 }
601
602 # stubbed/aliased:
603 __green() {
604 printf -- "%b" "$1"
605 }
606
607 __red() {
608 printf -- "%b" "$1"
609 }
610
611 _log() {
612 return 0
613 }
614
615 _info() {
616 printf -- "%s" "[$(date)] " >&1
617 echo "$1"
618 }
619
620 _err() {
621 printf -- "%s" "[$(date)] " >&2
622 if [ -z "$2" ]; then
623 __red "$1" >&2
624 else
625 __red "$1='$2'" >&2
626 fi
627 printf "\n" >&2
628 return 1
629 }
630
631 # key
632 _readaccountconf() {
633 echo "${!1}"
634 }
635
636 # key
637 _readaccountconf_mutable() {
638 _readaccountconf "$1"
639 }
640
641 # no-ops:
642 _clearaccountconf() {
643 return 0
644 }
645
646 _cleardomainconf() {
647 return 0
648 }
649
650 _debug() {
651 if [[ $DEBUG -eq 0 ]]; then
652 return
653 fi
654 printf -- "%s" "[$(date)] " >&1
655 echo "$1 $2"
656 }
657
658 _debug2() {
659 _debug $1 $2
660 }
661
662 _debug3() {
663 _debug $1 $2
664 }
665
666 _secure_debug() {
667 _debug $1 $2
668 }
669
670 _secure_debug2() {
671 _debug $1 $2
672 }
673
674 _secure_debug3() {
675 _debug $1 $2
676 }
677
678 _saveaccountconf() {
679 return 0
680 }
681
682 _saveaccountconf_mutable() {
683 return 0
684 }
685
686 _save_conf() {
687 return 0
688 }
689
690 _savedomainconf() {
691 return 0
692 }
693
694 _source_plugin_config() {
695 return 0
696 }
697
698 # Proxmox implementation to inject the DNSAPI variables
699 _load_plugin_config() {
700 while IFS= read -r line; do
701 ADDR=(${line/=/ })
702 key="${ADDR[0]}"
703 value="${ADDR[1]}"
704
705 # acme.sh uses eval insted of export
706 if [ -n "$key" ]; then
707 export "$key"="$value"
708 fi
709 done
710 }
711
712 # call setup and teardown direct
713 # the parameter must be set in the correct order
714 # $1 <String> DNS Plugin name
715 # $2 <String> Fully Qualified Domain Name
716 # $3 <String> value for TXT record
717 # $4 <String> DNS plugin auth and config parameter separated by ","
718 # $5 <Integer> 0 is off, and the default all others are on.
719
720 setup() {
721 dns_plugin="dns_$1"
722 dns_plugin_path="${DNS_PLUGIN_PATH}/${dns_plugin}.sh"
723 fqdn="_acme-challenge.$2"
724 DEBUG=$3
725 IFS= read -r txtvalue
726 plugin_conf_string=$4
727
728 _load_plugin_config
729
730 if ! . "$dns_plugin_path"; then
731 _err "Load file $dns_plugin error."
732 return 1
733 fi
734
735 addcommand="${dns_plugin}_add"
736 if ! _exists "$addcommand"; then
737 _err "It seems that your api file is not correct, it must have a function named: $addcommand"
738 return 1
739 fi
740
741 if ! $addcommand "$fqdn" "$txtvalue"; then
742 _err "Error add txt for domain:$fulldomain"
743 return 1
744 fi
745 }
746
747 teardown() {
748 dns_plugin="dns_$1"
749 dns_plugin_path="${DNS_PLUGIN_PATH}/${dns_plugin}.sh"
750 fqdn="_acme-challenge.$2"
751 DEBUG=$3
752 IFS= read -r txtvalue
753
754 _load_plugin_config
755
756 if ! . "$dns_plugin_path"; then
757 _err "Load file $dns_plugin error."
758 return 1
759 fi
760
761 rmcommand="${dns_plugin}_rm"
762 if ! _exists "$rmcommand"; then
763 _err "It seems that your api file is not correct, it must have a function named: $rmcommand"
764 return 1
765 fi
766
767 if ! $rmcommand "$fqdn" "$txtvalue"; then
768 _err "Error add txt for domain:$fulldomain"
769 return 1
770 fi
771 }
772
773 "$@"