]> git.proxmox.com Git - mirror_acme.sh.git/blob - le.sh
support ECC key, ECDSA certificate
[mirror_acme.sh.git] / le.sh
1 #!/bin/bash
2 VER=1.1.6
3 PROJECT="https://github.com/Neilpang/le"
4
5 DEFAULT_CA="https://acme-v01.api.letsencrypt.org"
6 DEFAULT_AGREEMENT="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
7
8 STAGE_CA="https://acme-staging.api.letsencrypt.org"
9
10 VTYPE_HTTP="http-01"
11 VTYPE_DNS="dns-01"
12
13 if [ -z "$AGREEMENT" ] ; then
14 AGREEMENT="$DEFAULT_AGREEMENT"
15 fi
16
17 _debug() {
18
19 if [ -z "$DEBUG" ] ; then
20 return
21 fi
22
23 if [ -z "$2" ] ; then
24 echo $1
25 else
26 echo "$1"="$2"
27 fi
28 }
29
30 _info() {
31 if [ -z "$2" ] ; then
32 echo "$1"
33 else
34 echo "$1"="$2"
35 fi
36 }
37
38 _err() {
39 if [ -z "$2" ] ; then
40 echo "$1" >&2
41 else
42 echo "$1"="$2" >&2
43 fi
44 return 1
45 }
46
47 _h2b() {
48 hex=$(cat)
49 i=1
50 j=2
51 while [ '1' ] ; do
52 h=$(printf $hex | cut -c $i-$j)
53 if [ -z "$h" ] ; then
54 break;
55 fi
56 printf "\x$h"
57 let "i+=2"
58 let "j+=2"
59 done
60 }
61
62 _base64() {
63 openssl base64 -e | tr -d '\n'
64 }
65
66 #domain [2048]
67 createAccountKey() {
68 _info "Creating account key"
69 if [ -z "$1" ] ; then
70 echo Usage: createAccountKey account-domain [2048]
71 return
72 fi
73
74 account=$1
75 length=$2
76
77 if [[ "$length" == "ec-"* ]] ; then
78 length=2048
79 fi
80
81 if [ -z "$2" ] ; then
82 _info "Use default length 2048"
83 length=2048
84 fi
85 _initpath
86
87 if [ -f "$ACCOUNT_KEY_PATH" ] ; then
88 _info "Account key exists, skip"
89 return
90 else
91 #generate account key
92 openssl genrsa $length > "$ACCOUNT_KEY_PATH"
93 fi
94
95 }
96
97 #domain length
98 createDomainKey() {
99 _info "Creating domain key"
100 if [ -z "$1" ] ; then
101 echo Usage: createDomainKey domain [2048]
102 return
103 fi
104
105 domain=$1
106 length=$2
107 isec=""
108 if [[ "$length" == "ec-"* ]] ; then
109 isec="1"
110 length=$(printf $length | cut -d '-' -f 2-100)
111 eccname="$length"
112 fi
113
114 if [ -z "$length" ] ; then
115 if [ "$isec" ] ; then
116 length=256
117 else
118 length=2048
119 fi
120 fi
121 _info "Use length $length"
122
123 if [ "$isec" ] ; then
124 if [ "$length" == "256" ] ; then
125 eccname="prime256v1"
126 fi
127 if [ "$length" == "384" ] ; then
128 eccname="secp384r1"
129 fi
130 if [ "$length" == "521" ] ; then
131 eccname="secp521r1"
132 fi
133 _info "Using ec name: $eccname"
134 fi
135
136 _initpath $domain
137
138 if [ ! -f "$CERT_KEY_PATH" ] || ( [ "$FORCE" ] && ! [ "$IS_RENEW" ] ); then
139 #generate account key
140 if [ "$isec" ] ; then
141 openssl ecparam -name $eccname -genkey 2>/dev/null > "$CERT_KEY_PATH"
142 else
143 openssl genrsa $length 2>/dev/null > "$CERT_KEY_PATH"
144 fi
145 else
146 if [ "$IS_RENEW" ] ; then
147 _info "Domain key exists, skip"
148 return 0
149 else
150 _err "Domain key exists, do you want to overwrite the key?"
151 _err "Set FORCE=1, and try again."
152 return 1
153 fi
154 fi
155
156 }
157
158 # domain domainlist
159 createCSR() {
160 _info "Creating csr"
161 if [ -z "$1" ] ; then
162 echo Usage: $0 domain [domainlist]
163 return
164 fi
165 domain=$1
166 _initpath $domain
167
168 domainlist=$2
169
170 if [ -f "$CSR_PATH" ] && [ "$IS_RENEW" ] && ! [ "$FORCE" ]; then
171 _info "CSR exists, skip"
172 return
173 fi
174
175 if [ -z "$domainlist" ] ; then
176 #single domain
177 _info "Single domain" $domain
178 openssl req -new -sha256 -key "$CERT_KEY_PATH" -subj "/CN=$domain" > "$CSR_PATH"
179 else
180 alt="DNS:$(echo $domainlist | sed "s/,/,DNS:/g")"
181 #multi
182 _info "Multi domain" "$alt"
183 openssl req -new -sha256 -key "$CERT_KEY_PATH" -subj "/CN=$domain" -reqexts SAN -config <(printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\n[SAN]\nsubjectAltName=$alt") -out "$CSR_PATH"
184 fi
185
186 }
187
188 _b64() {
189 __n=$(cat)
190 echo $__n | tr '/+' '_-' | tr -d '= '
191 }
192
193 _send_signed_request() {
194 url=$1
195 payload=$2
196 needbase64=$3
197
198 _debug url $url
199 _debug payload "$payload"
200
201 CURL_HEADER="$LE_WORKING_DIR/curl.header"
202 dp="$LE_WORKING_DIR/curl.dump"
203 CURL="curl --silent --dump-header $CURL_HEADER "
204 if [ "$DEBUG" ] ; then
205 CURL="$CURL --trace-ascii $dp "
206 fi
207 payload64=$(echo -n $payload | _base64 | _b64)
208 _debug payload64 $payload64
209
210 nonceurl="$API/directory"
211 nonce=$($CURL -I $nonceurl | grep "^Replay-Nonce:" | sed s/\\r//|sed s/\\n//| cut -d ' ' -f 2)
212
213 _debug nonce $nonce
214
215 protected=$(echo -n "$HEADERPLACE" | sed "s/NONCE/$nonce/" )
216 _debug protected "$protected"
217
218 protected64=$( echo -n $protected | _base64 | _b64)
219 _debug protected64 "$protected64"
220
221 sig=$(echo -n "$protected64.$payload64" | openssl dgst -sha256 -sign $ACCOUNT_KEY_PATH | _base64 | _b64)
222 _debug sig "$sig"
223
224 body="{\"header\": $HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}"
225 _debug body "$body"
226
227 if [ "$needbase64" ] ; then
228 response="$($CURL -X POST --data "$body" $url | _base64)"
229 else
230 response="$($CURL -X POST --data "$body" $url)"
231 fi
232
233 responseHeaders="$(sed 's/\r//g' $CURL_HEADER)"
234
235 _debug responseHeaders "$responseHeaders"
236 _debug response "$response"
237 code="$(grep ^HTTP $CURL_HEADER | tail -1 | cut -d " " -f 2)"
238 _debug code $code
239
240 }
241
242 _get() {
243 url="$1"
244 _debug url $url
245 response="$(curl --silent $url)"
246 ret=$?
247 _debug response "$response"
248 code="$(echo $response | grep -o '"status":[0-9]\+' | cut -d : -f 2)"
249 _debug code $code
250 return $ret
251 }
252
253 #setopt "file" "opt" "=" "value" [";"]
254 _setopt() {
255 __conf="$1"
256 __opt="$2"
257 __sep="$3"
258 __val="$4"
259 __end="$5"
260 if [ -z "$__opt" ] ; then
261 echo usage: $0 '"file" "opt" "=" "value" [";"]'
262 return
263 fi
264 if [ ! -f "$__conf" ] ; then
265 touch "$__conf"
266 fi
267 if grep -H -n "^$__opt$__sep" "$__conf" > /dev/null ; then
268 _debug OK
269 if [[ "$__val" == *"&"* ]] ; then
270 __val="$(echo $__val | sed 's/&/\\&/g')"
271 fi
272 sed -i "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" "$__conf"
273 else
274 _debug APP
275 echo "$__opt$__sep$__val$__end" >> "$__conf"
276 fi
277 _debug "$(grep -H -n "^$__opt$__sep" $__conf)"
278 }
279
280 #_savedomainconf key value
281 #save to domain.conf
282 _savedomainconf() {
283 key="$1"
284 value="$2"
285 if [ "$DOMAIN_CONF" ] ; then
286 _setopt $DOMAIN_CONF "$key" "=" "$value"
287 else
288 _err "DOMAIN_CONF is empty, can not save $key=$value"
289 fi
290 }
291
292 #_saveaccountconf key value
293 _saveaccountconf() {
294 key="$1"
295 value="$2"
296 if [ "$ACCOUNT_CONF_PATH" ] ; then
297 _setopt $ACCOUNT_CONF_PATH "$key" "=" "$value"
298 else
299 _err "ACCOUNT_CONF_PATH is empty, can not save $key=$value"
300 fi
301 }
302
303 _startserver() {
304 content="$1"
305 _NC="nc -q 1"
306 if nc -h 2>&1 | grep "nmap.org/ncat" >/dev/null ; then
307 _NC="nc"
308 fi
309 # while true ; do
310 if [ "$DEBUG" ] ; then
311 echo -e -n "HTTP/1.1 200 OK\r\n\r\n$content" | $_NC -l -p 80 -vv
312 else
313 echo -e -n "HTTP/1.1 200 OK\r\n\r\n$content" | $_NC -l -p 80 > /dev/null
314 fi
315 # done
316 }
317
318 _stopserver() {
319 pid="$1"
320
321 }
322
323 _initpath() {
324
325 #check if there is sudo installed, AND if the current user is a sudoer.
326 if command -v sudo > /dev/null ; then
327 if [ "$(sudo -n uptime 2>&1|grep "load"|wc -l)" != "0" ] ; then
328 SUDO=sudo
329 fi
330 fi
331
332 if [ -z "$LE_WORKING_DIR" ]; then
333 LE_WORKING_DIR=$HOME/.le
334 fi
335
336 if [ -z "$ACCOUNT_CONF_PATH" ] ; then
337 ACCOUNT_CONF_PATH="$LE_WORKING_DIR/account.conf"
338 fi
339
340 if [ -f "$ACCOUNT_CONF_PATH" ] ; then
341 source "$ACCOUNT_CONF_PATH"
342 fi
343
344 if [ -z "$API" ] ; then
345 if [ -z "$STAGE" ] ; then
346 API="$DEFAULT_CA"
347 else
348 API="$STAGE_CA"
349 _info "Using stage api:$API"
350 fi
351 fi
352
353 if [ -z "$ACME_DIR" ] ; then
354 ACME_DIR="/home/.acme"
355 fi
356
357 if [ -z "$APACHE_CONF_BACKUP_DIR" ] ; then
358 APACHE_CONF_BACKUP_DIR="$LE_WORKING_DIR/"
359 fi
360
361 domain="$1"
362 mkdir -p "$LE_WORKING_DIR"
363
364 if [ -z "$ACCOUNT_KEY_PATH" ] ; then
365 ACCOUNT_KEY_PATH="$LE_WORKING_DIR/account.key"
366 fi
367
368 if [ -z "$domain" ] ; then
369 return 0
370 fi
371
372 domainhome="$LE_WORKING_DIR/$domain"
373 mkdir -p "$domainhome"
374
375 if [ -z "$DOMAIN_CONF" ] ; then
376 DOMAIN_CONF="$domainhome/$Le_Domain.conf"
377 fi
378
379 if [ -z "$CSR_PATH" ] ; then
380 CSR_PATH="$domainhome/$domain.csr"
381 fi
382 if [ -z "$CERT_KEY_PATH" ] ; then
383 CERT_KEY_PATH="$domainhome/$domain.key"
384 fi
385 if [ -z "$CERT_PATH" ] ; then
386 CERT_PATH="$domainhome/$domain.cer"
387 fi
388 if [ -z "$CA_CERT_PATH" ] ; then
389 CA_CERT_PATH="$domainhome/ca.cer"
390 fi
391
392 }
393
394
395 _apachePath() {
396 httpdroot="$(apachectl -V | grep HTTPD_ROOT= | cut -d = -f 2 | sed s/\"//g)"
397 httpdconfname="$(apachectl -V | grep SERVER_CONFIG_FILE= | cut -d = -f 2 | sed s/\"//g)"
398 httpdconf="$httpdroot/$httpdconfname"
399 if [ ! -f $httpdconf ] ; then
400 _err "Apache Config file not found" $httpdconf
401 return 1
402 fi
403 return 0
404 }
405
406 _restoreApache() {
407 if [ -z "$usingApache" ] ; then
408 return 0
409 fi
410 _initpath
411 if ! _apachePath ; then
412 return 1
413 fi
414
415 if [ ! -f "$APACHE_CONF_BACKUP_DIR/$httpdconfname" ] ; then
416 _debug "No config file to restore."
417 return 0
418 fi
419
420 cp -p "$APACHE_CONF_BACKUP_DIR/$httpdconfname" "$httpdconf"
421 if ! apachectl -t ; then
422 _err "Sorry, restore apache config error, please contact me."
423 return 1;
424 fi
425 rm -f "$APACHE_CONF_BACKUP_DIR/$httpdconfname"
426 return 0
427 }
428
429 _setApache() {
430 _initpath
431 if ! _apachePath ; then
432 return 1
433 fi
434
435 #backup the conf
436 _debug "Backup apache config file" $httpdconf
437 cp -p $httpdconf $APACHE_CONF_BACKUP_DIR/
438 _info "JFYI, Config file $httpdconf is backuped to $APACHE_CONF_BACKUP_DIR/$httpdconfname"
439 _info "In case there is an error that can not be restored automatically, you may try restore it yourself."
440 _info "The backup file will be deleted on sucess, just forget it."
441
442 #add alias
443 echo "
444 Alias /.well-known/acme-challenge $ACME_DIR
445
446 <Directory $ACME_DIR >
447 Require all granted
448 </Directory>
449 " >> $httpdconf
450
451 if ! apachectl -t ; then
452 _err "Sorry, apache config error, please contact me."
453 _restoreApache
454 return 1;
455 fi
456
457 if [ ! -d "$ACME_DIR" ] ; then
458 mkdir -p "$ACME_DIR"
459 chmod 755 "$ACME_DIR"
460 fi
461
462 if ! apachectl graceful ; then
463 _err "Sorry, apachectl graceful error, please contact me."
464 _restoreApache
465 return 1;
466 fi
467 usingApache="1"
468 return 0
469 }
470
471 _clearup () {
472 _stopserver $serverproc
473 serverproc=""
474 _restoreApache
475 }
476
477 # webroot removelevel tokenfile
478 _clearupwebbroot() {
479 __webroot="$1"
480 if [ -z "$__webroot" ] ; then
481 _debug "no webroot specified, skip"
482 return 0
483 fi
484
485 if [ "$2" == '1' ] ; then
486 _debug "remove $__webroot/.well-known"
487 rm -rf "$__webroot/.well-known"
488 elif [ "$2" == '2' ] ; then
489 _debug "remove $__webroot/.well-known/acme-challenge"
490 rm -rf "$__webroot/.well-known/acme-challenge"
491 elif [ "$2" == '3' ] ; then
492 _debug "remove $__webroot/.well-known/acme-challenge/$3"
493 rm -rf "$__webroot/.well-known/acme-challenge/$3"
494 else
495 _info "Skip for removelevel:$2"
496 fi
497
498 return 0
499
500 }
501
502 issue() {
503 if [ -z "$2" ] ; then
504 _err "Usage: le issue webroot|no|apache|dns a.com [www.a.com,b.com,c.com]|no [key-length]|no"
505 return 1
506 fi
507 Le_Webroot="$1"
508 Le_Domain="$2"
509 Le_Alt="$3"
510 Le_Keylength="$4"
511 Le_RealCertPath="$5"
512 Le_RealKeyPath="$6"
513 Le_RealCACertPath="$7"
514 Le_ReloadCmd="$8"
515
516
517 _initpath $Le_Domain
518
519 if [ -f "$DOMAIN_CONF" ] ; then
520 Le_NextRenewTime=$(grep "^Le_NextRenewTime=" "$DOMAIN_CONF" | cut -d '=' -f 2)
521 if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(date -u "+%s" )" -lt "$Le_NextRenewTime" ] ; then
522 _info "Skip, Next renewal time is: $(grep "^Le_NextRenewTimeStr" "$DOMAIN_CONF" | cut -d '=' -f 2)"
523 return 2
524 fi
525 fi
526
527 if [ "$Le_Alt" == "no" ] ; then
528 Le_Alt=""
529 fi
530 if [ "$Le_Keylength" == "no" ] ; then
531 Le_Keylength=""
532 fi
533 if [ "$Le_RealCertPath" == "no" ] ; then
534 Le_RealCertPath=""
535 fi
536 if [ "$Le_RealKeyPath" == "no" ] ; then
537 Le_RealKeyPath=""
538 fi
539 if [ "$Le_RealCACertPath" == "no" ] ; then
540 Le_RealCACertPath=""
541 fi
542 if [ "$Le_ReloadCmd" == "no" ] ; then
543 Le_ReloadCmd=""
544 fi
545
546 _setopt "$DOMAIN_CONF" "Le_Domain" "=" "$Le_Domain"
547 _setopt "$DOMAIN_CONF" "Le_Alt" "=" "$Le_Alt"
548 _setopt "$DOMAIN_CONF" "Le_Webroot" "=" "$Le_Webroot"
549 _setopt "$DOMAIN_CONF" "Le_Keylength" "=" "$Le_Keylength"
550 _setopt "$DOMAIN_CONF" "Le_RealCertPath" "=" "\"$Le_RealCertPath\""
551 _setopt "$DOMAIN_CONF" "Le_RealCACertPath" "=" "\"$Le_RealCACertPath\""
552 _setopt "$DOMAIN_CONF" "Le_RealKeyPath" "=" "\"$Le_RealKeyPath\""
553 _setopt "$DOMAIN_CONF" "Le_ReloadCmd" "=" "\"$Le_ReloadCmd\""
554
555 if [ "$Le_Webroot" == "no" ] ; then
556 _info "Standalone mode."
557 if ! command -v "nc" > /dev/null ; then
558 _err "Please install netcat(nc) tools first."
559 return 1
560 fi
561
562 netprc="$(ss -ntpl | grep ':80 ')"
563 if [ "$netprc" ] ; then
564 _err "$netprc"
565 _err "tcp port 80 is already used by $(echo "$netprc" | cut -d : -f 4)"
566 _err "Please stop it first"
567 return 1
568 fi
569 fi
570
571 if [ "$Le_Webroot" == "apache" ] ; then
572 if ! _setApache ; then
573 _err "set up apache error. Report error to me."
574 return 1
575 fi
576 wellknown_path="$ACME_DIR"
577 else
578 usingApache=""
579 fi
580
581 createAccountKey $Le_Domain $Le_Keylength
582
583 if ! createDomainKey $Le_Domain $Le_Keylength ; then
584 _err "Create domain key error."
585 return 1
586 fi
587
588 if ! createCSR $Le_Domain $Le_Alt ; then
589 _err "Create CSR error."
590 return 1
591 fi
592
593 pub_exp=$(openssl rsa -in $ACCOUNT_KEY_PATH -noout -text | grep "^publicExponent:"| cut -d '(' -f 2 | cut -d 'x' -f 2 | cut -d ')' -f 1)
594 if [ "${#pub_exp}" == "5" ] ; then
595 pub_exp=0$pub_exp
596 fi
597 _debug pub_exp "$pub_exp"
598
599 e=$(echo $pub_exp | _h2b | _base64)
600 _debug e "$e"
601
602 modulus=$(openssl rsa -in $ACCOUNT_KEY_PATH -modulus -noout | cut -d '=' -f 2 )
603 n=$(echo $modulus| _h2b | _base64 | _b64 )
604
605 jwk='{"e": "'$e'", "kty": "RSA", "n": "'$n'"}'
606
607 HEADER='{"alg": "RS256", "jwk": '$jwk'}'
608 HEADERPLACE='{"nonce": "NONCE", "alg": "RS256", "jwk": '$jwk'}'
609 _debug HEADER "$HEADER"
610
611 accountkey_json=$(echo -n "$jwk" | sed "s/ //g")
612 thumbprint=$(echo -n "$accountkey_json" | openssl dgst -sha256 -binary | _base64 | _b64)
613
614
615 _info "Registering account"
616 regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}'
617 if [ "$ACCOUNT_EMAIL" ] ; then
618 regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}'
619 fi
620 _send_signed_request "$API/acme/new-reg" "$regjson"
621
622 if [ "$code" == "" ] || [ "$code" == '201' ] ; then
623 _info "Registered"
624 echo $response > $LE_WORKING_DIR/account.json
625 elif [ "$code" == '409' ] ; then
626 _info "Already registered"
627 else
628 _err "Register account Error."
629 _clearup
630 return 1
631 fi
632
633 vtype="$VTYPE_HTTP"
634 if [[ "$Le_Webroot" == "dns"* ]] ; then
635 vtype="$VTYPE_DNS"
636 fi
637
638 vlist="$Le_Vlist"
639 # verify each domain
640 _info "Verify each domain"
641 sep='#'
642 if [ -z "$vlist" ] ; then
643 alldomains=$(echo "$Le_Domain,$Le_Alt" | sed "s/,/ /g")
644 for d in $alldomains
645 do
646 _info "Geting token for domain" $d
647 _send_signed_request "$API/acme/new-authz" "{\"resource\": \"new-authz\", \"identifier\": {\"type\": \"dns\", \"value\": \"$d\"}}"
648 if [ ! -z "$code" ] && [ ! "$code" == '201' ] ; then
649 _err "new-authz error: $response"
650 _clearup
651 return 1
652 fi
653
654 entry=$(echo $response | egrep -o '{[^{]*"type":"'$vtype'"[^}]*')
655 _debug entry "$entry"
656
657 token=$(echo "$entry" | sed 's/,/\n'/g| grep '"token":'| cut -d : -f 2|sed 's/"//g')
658 _debug token $token
659
660 uri=$(echo "$entry" | sed 's/,/\n'/g| grep '"uri":'| cut -d : -f 2,3|sed 's/"//g')
661 _debug uri $uri
662
663 keyauthorization="$token.$thumbprint"
664 _debug keyauthorization "$keyauthorization"
665
666 dvlist="$d$sep$keyauthorization$sep$uri"
667 _debug dvlist "$dvlist"
668
669 vlist="$vlist$dvlist,"
670
671 done
672
673 #add entry
674 dnsadded=""
675 ventries=$(echo "$vlist" | sed "s/,/ /g")
676 for ventry in $ventries
677 do
678 d=$(echo $ventry | cut -d $sep -f 1)
679 keyauthorization=$(echo $ventry | cut -d $sep -f 2)
680
681 if [ "$vtype" == "$VTYPE_DNS" ] ; then
682 dnsadded='0'
683 txtdomain="_acme-challenge.$d"
684 _debug txtdomain "$txtdomain"
685 txt="$(echo -e -n $keyauthorization | openssl sha -sha256 -binary | _base64 | _b64)"
686 _debug txt "$txt"
687 #dns
688 #1. check use api
689 d_api=""
690 if [ -f "$LE_WORKING_DIR/$d/$Le_Webroot" ] ; then
691 d_api="$LE_WORKING_DIR/$d/$Le_Webroot"
692 elif [ -f "$LE_WORKING_DIR/$d/$Le_Webroot.sh" ] ; then
693 d_api="$LE_WORKING_DIR/$d/$Le_Webroot.sh"
694 elif [ -f "$LE_WORKING_DIR/$Le_Webroot" ] ; then
695 d_api="$LE_WORKING_DIR/$Le_Webroot"
696 elif [ -f "$LE_WORKING_DIR/$Le_Webroot.sh" ] ; then
697 d_api="$LE_WORKING_DIR/$Le_Webroot.sh"
698 elif [ -f "$LE_WORKING_DIR/dnsapi/$Le_Webroot" ] ; then
699 d_api="$LE_WORKING_DIR/dnsapi/$Le_Webroot"
700 elif [ -f "$LE_WORKING_DIR/dnsapi/$Le_Webroot.sh" ] ; then
701 d_api="$LE_WORKING_DIR/dnsapi/$Le_Webroot.sh"
702 fi
703 _debug d_api "$d_api"
704
705 if [ "$d_api" ]; then
706 _info "Found domain api file: $d_api"
707 else
708 _err "Add the following TXT record:"
709 _err "Domain: $txtdomain"
710 _err "TXT value: $txt"
711 _err "Please be aware that you prepend _acme-challenge. before your domain"
712 _err "so the resulting subdomain will be: $txtdomain"
713 continue
714 fi
715
716 if ! source $d_api ; then
717 _err "Load file $d_api error. Please check your api file and try again."
718 return 1
719 fi
720
721 addcommand="$Le_Webroot-add"
722 if ! command -v $addcommand ; then
723 _err "It seems that your api file is not correct, it must have a function named: $Le_Webroot"
724 return 1
725 fi
726
727 if ! $addcommand $txtdomain $txt ; then
728 _err "Error add txt for domain:$txtdomain"
729 return 1
730 fi
731 dnsadded='1'
732 fi
733 done
734
735 if [ "$dnsadded" == '0' ] ; then
736 _setopt "$DOMAIN_CONF" "Le_Vlist" "=" "\"$vlist\""
737 _debug "Dns record not added yet, so, save to $DOMAIN_CONF and exit."
738 _err "Please add the TXT records to the domains, and retry again."
739 return 1
740 fi
741
742 fi
743
744 if [ "$dnsadded" == '1' ] ; then
745 _info "Sleep 60 seconds for the txt records to take effect"
746 sleep 60
747 fi
748
749 _debug "ok, let's start to verify"
750 ventries=$(echo "$vlist" | sed "s/,/ /g")
751 for ventry in $ventries
752 do
753 d=$(echo $ventry | cut -d $sep -f 1)
754 keyauthorization=$(echo $ventry | cut -d $sep -f 2)
755 uri=$(echo $ventry | cut -d $sep -f 3)
756 _info "Verifying:$d"
757 _debug "d" "$d"
758 _debug "keyauthorization" "$keyauthorization"
759 _debug "uri" "$uri"
760 removelevel=""
761 token=""
762 if [ "$vtype" == "$VTYPE_HTTP" ] ; then
763 if [ "$Le_Webroot" == "no" ] ; then
764 _info "Standalone mode server"
765 _startserver "$keyauthorization" &
766 serverproc="$!"
767 sleep 2
768 _debug serverproc $serverproc
769 else
770 if [ -z "$wellknown_path" ] ; then
771 wellknown_path="$Le_Webroot/.well-known/acme-challenge"
772 fi
773 _debug wellknown_path "$wellknown_path"
774
775 if [ ! -d "$Le_Webroot/.well-known" ] ; then
776 removelevel='1'
777 elif [ ! -d "$Le_Webroot/.well-known/acme-challenge" ] ; then
778 removelevel='2'
779 else
780 removelevel='3'
781 fi
782
783 token="$(echo -e -n "$keyauthorization" | cut -d '.' -f 1)"
784 _debug "writing token:$token to $wellknown_path/$token"
785
786 mkdir -p "$wellknown_path"
787 echo -n "$keyauthorization" > "$wellknown_path/$token"
788
789 webroot_owner=$(stat -c '%U:%G' $Le_Webroot)
790 _debug "Changing owner/group of .well-known to $webroot_owner"
791 chown -R $webroot_owner "$Le_Webroot/.well-known"
792
793 fi
794 fi
795
796 _send_signed_request $uri "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}"
797
798 if [ ! -z "$code" ] && [ ! "$code" == '202' ] ; then
799 _err "$d:Challenge error: $resource"
800 _clearupwebbroot "$Le_Webroot" "$removelevel" "$token"
801 _clearup
802 return 1
803 fi
804
805 while [ "1" ] ; do
806 _debug "sleep 5 secs to verify"
807 sleep 5
808 _debug "checking"
809
810 if ! _get $uri ; then
811 _err "$d:Verify error:$resource"
812 _clearupwebbroot "$Le_Webroot" "$removelevel" "$token"
813 _clearup
814 return 1
815 fi
816
817 status=$(echo $response | egrep -o '"status":"[^"]+"' | cut -d : -f 2 | sed 's/"//g')
818 if [ "$status" == "valid" ] ; then
819 _info "Success"
820 _stopserver $serverproc
821 serverproc=""
822 _clearupwebbroot "$Le_Webroot" "$removelevel" "$token"
823 break;
824 fi
825
826 if [ "$status" == "invalid" ] ; then
827 error=$(echo $response | egrep -o '"error":{[^}]*}' | grep -o '"detail":"[^"]*"' | cut -d '"' -f 4)
828 _err "$d:Verify error:$error"
829 _clearupwebbroot "$Le_Webroot" "$removelevel" "$token"
830 _clearup
831 return 1;
832 fi
833
834 if [ "$status" == "pending" ] ; then
835 _info "Pending"
836 else
837 _err "$d:Verify error:$response"
838 _clearupwebbroot "$Le_Webroot" "$removelevel" "$token"
839 _clearup
840 return 1
841 fi
842
843 done
844
845 done
846
847 _clearup
848 _info "Verify finished, start to sign."
849 der="$(openssl req -in $CSR_PATH -outform DER | _base64 | _b64)"
850 _send_signed_request "$API/acme/new-cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64"
851
852
853 Le_LinkCert="$(grep -i -o '^Location.*' $CURL_HEADER |sed 's/\r//g'| cut -d " " -f 2)"
854 _setopt "$DOMAIN_CONF" "Le_LinkCert" "=" "$Le_LinkCert"
855
856 if [ "$Le_LinkCert" ] ; then
857 echo -----BEGIN CERTIFICATE----- > "$CERT_PATH"
858 curl --silent "$Le_LinkCert" | openssl base64 -e >> "$CERT_PATH"
859 echo -----END CERTIFICATE----- >> "$CERT_PATH"
860 _info "Cert success."
861 cat "$CERT_PATH"
862
863 _info "Your cert is in $CERT_PATH"
864 fi
865
866
867 if [ -z "$Le_LinkCert" ] ; then
868 response="$(echo $response | openssl base64 -d -A)"
869 _err "Sign failed: $(echo "$response" | grep -o '"detail":"[^"]*"')"
870 return 1
871 fi
872
873 _setopt "$DOMAIN_CONF" 'Le_Vlist' '=' "\"\""
874
875 Le_LinkIssuer=$(grep -i '^Link' $CURL_HEADER | cut -d " " -f 2| cut -d ';' -f 1 | sed 's/<//g' | sed 's/>//g')
876 _setopt "$DOMAIN_CONF" "Le_LinkIssuer" "=" "$Le_LinkIssuer"
877
878 if [ "$Le_LinkIssuer" ] ; then
879 echo -----BEGIN CERTIFICATE----- > "$CA_CERT_PATH"
880 curl --silent "$Le_LinkIssuer" | openssl base64 -e >> "$CA_CERT_PATH"
881 echo -----END CERTIFICATE----- >> "$CA_CERT_PATH"
882 _info "The intermediate CA cert is in $CA_CERT_PATH"
883 fi
884
885 Le_CertCreateTime=$(date -u "+%s")
886 _setopt "$DOMAIN_CONF" "Le_CertCreateTime" "=" "$Le_CertCreateTime"
887
888 Le_CertCreateTimeStr=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
889 _setopt "$DOMAIN_CONF" "Le_CertCreateTimeStr" "=" "\"$Le_CertCreateTimeStr\""
890
891 if [ ! "$Le_RenewalDays" ] ; then
892 Le_RenewalDays=80
893 fi
894
895 _setopt "$DOMAIN_CONF" "Le_RenewalDays" "=" "$Le_RenewalDays"
896
897 Le_NextRenewTime=$(date -u -d "+$Le_RenewalDays day" "+%s")
898 _setopt "$DOMAIN_CONF" "Le_NextRenewTime" "=" "$Le_NextRenewTime"
899
900 Le_NextRenewTimeStr=$(date -u -d "+$Le_RenewalDays day" "+%Y-%m-%d %H:%M:%S UTC")
901 _setopt "$DOMAIN_CONF" "Le_NextRenewTimeStr" "=" "\"$Le_NextRenewTimeStr\""
902
903
904 installcert $Le_Domain "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd"
905
906 }
907
908 renew() {
909 Le_Domain="$1"
910 if [ -z "$Le_Domain" ] ; then
911 _err "Usage: $0 domain.com"
912 return 1
913 fi
914
915 _initpath $Le_Domain
916
917 if [ ! -f "$DOMAIN_CONF" ] ; then
918 _info "$Le_Domain is not a issued domain, skip."
919 return 0;
920 fi
921
922 source "$DOMAIN_CONF"
923 if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(date -u "+%s" )" -lt "$Le_NextRenewTime" ] ; then
924 _info "Skip, Next renewal time is: $Le_NextRenewTimeStr"
925 return 2
926 fi
927
928 IS_RENEW="1"
929 issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd"
930 local res=$?
931 IS_RENEW=""
932
933 return $res
934 }
935
936 renewAll() {
937 _initpath
938 _info "renewAll"
939
940 for d in $(ls -F $LE_WORKING_DIR | grep [^.].*[.].*/$ ) ; do
941 d=$(echo $d | cut -d '/' -f 1)
942 _info "renew $d"
943
944 Le_LinkCert=""
945 Le_Domain=""
946 Le_Alt=""
947 Le_Webroot=""
948 Le_Keylength=""
949 Le_LinkIssuer=""
950
951 Le_CertCreateTime=""
952 Le_CertCreateTimeStr=""
953 Le_RenewalDays=""
954 Le_NextRenewTime=""
955 Le_NextRenewTimeStr=""
956
957 Le_RealCertPath=""
958 Le_RealKeyPath=""
959
960 Le_RealCACertPath=""
961
962 Le_ReloadCmd=""
963
964 DOMAIN_CONF=""
965 CSR_PATH=""
966 CERT_KEY_PATH=""
967 CERT_PATH=""
968 CA_CERT_PATH=""
969 ACCOUNT_KEY_PATH=""
970
971 wellknown_path=""
972
973 renew "$d"
974 done
975
976 }
977
978 installcert() {
979 Le_Domain="$1"
980 if [ -z "$Le_Domain" ] ; then
981 _err "Usage: $0 domain.com [cert-file-path]|no [key-file-path]|no [ca-cert-file-path]|no [reloadCmd]|no"
982 return 1
983 fi
984
985 Le_RealCertPath="$2"
986 Le_RealKeyPath="$3"
987 Le_RealCACertPath="$4"
988 Le_ReloadCmd="$5"
989
990 _initpath $Le_Domain
991
992 _setopt "$DOMAIN_CONF" "Le_RealCertPath" "=" "\"$Le_RealCertPath\""
993 _setopt "$DOMAIN_CONF" "Le_RealCACertPath" "=" "\"$Le_RealCACertPath\""
994 _setopt "$DOMAIN_CONF" "Le_RealKeyPath" "=" "\"$Le_RealKeyPath\""
995 _setopt "$DOMAIN_CONF" "Le_ReloadCmd" "=" "\"$Le_ReloadCmd\""
996
997 if [ "$Le_RealCertPath" ] ; then
998 if [ -f "$Le_RealCertPath" ] ; then
999 cp -p "$Le_RealCertPath" "$Le_RealCertPath".bak
1000 fi
1001 cat "$CERT_PATH" > "$Le_RealCertPath"
1002 fi
1003
1004 if [ "$Le_RealCACertPath" ] ; then
1005 if [ -f "$Le_RealCACertPath" ] ; then
1006 cp -p "$Le_RealCACertPath" "$Le_RealCACertPath".bak
1007 fi
1008 if [ "$Le_RealCACertPath" == "$Le_RealCertPath" ] ; then
1009 echo "" >> "$Le_RealCACertPath"
1010 cat "$CA_CERT_PATH" >> "$Le_RealCACertPath"
1011 else
1012 cat "$CA_CERT_PATH" > "$Le_RealCACertPath"
1013 fi
1014 fi
1015
1016
1017 if [ "$Le_RealKeyPath" ] ; then
1018 if [ -f "$Le_RealKeyPath" ] ; then
1019 cp -p "$Le_RealKeyPath" "$Le_RealKeyPath".bak
1020 fi
1021 cat "$CERT_KEY_PATH" > "$Le_RealKeyPath"
1022 fi
1023
1024 if [ "$Le_ReloadCmd" ] ; then
1025 _info "Run Le_ReloadCmd: $Le_ReloadCmd"
1026 $Le_ReloadCmd
1027 fi
1028
1029 }
1030
1031 installcronjob() {
1032 _initpath
1033 _info "Installing cron job"
1034 if ! crontab -l | grep 'le.sh cron' ; then
1035 if [ -f "$LE_WORKING_DIR/le.sh" ] ; then
1036 lesh="\"$LE_WORKING_DIR\"/le.sh"
1037 else
1038 _err "Can not install cronjob, le.sh not found."
1039 return 1
1040 fi
1041 crontab -l | { cat; echo "0 0 * * * $SUDO LE_WORKING_DIR=\"$LE_WORKING_DIR\" $lesh cron > /dev/null"; } | crontab -
1042 fi
1043 return 0
1044 }
1045
1046 uninstallcronjob() {
1047 _info "Removing cron job"
1048 cr="$(crontab -l | grep 'le.sh cron')"
1049 if [ "$cr" ] ; then
1050 crontab -l | sed "/le.sh cron/d" | crontab -
1051 LE_WORKING_DIR="$(echo "$cr" | cut -d ' ' -f 7 | cut -d '=' -f 2 | tr -d '"')"
1052 _info LE_WORKING_DIR "$LE_WORKING_DIR"
1053 fi
1054 _initpath
1055
1056 }
1057
1058
1059 # Detect profile file if not specified as environment variable
1060 _detect_profile() {
1061 if [ -n "$PROFILE" -a -f "$PROFILE" ]; then
1062 echo "$PROFILE"
1063 return
1064 fi
1065
1066 local DETECTED_PROFILE
1067 DETECTED_PROFILE=''
1068 local SHELLTYPE
1069 SHELLTYPE="$(basename "/$SHELL")"
1070
1071 if [ "$SHELLTYPE" = "bash" ]; then
1072 if [ -f "$HOME/.bashrc" ]; then
1073 DETECTED_PROFILE="$HOME/.bashrc"
1074 elif [ -f "$HOME/.bash_profile" ]; then
1075 DETECTED_PROFILE="$HOME/.bash_profile"
1076 fi
1077 elif [ "$SHELLTYPE" = "zsh" ]; then
1078 DETECTED_PROFILE="$HOME/.zshrc"
1079 fi
1080
1081 if [ -z "$DETECTED_PROFILE" ]; then
1082 if [ -f "$HOME/.profile" ]; then
1083 DETECTED_PROFILE="$HOME/.profile"
1084 elif [ -f "$HOME/.bashrc" ]; then
1085 DETECTED_PROFILE="$HOME/.bashrc"
1086 elif [ -f "$HOME/.bash_profile" ]; then
1087 DETECTED_PROFILE="$HOME/.bash_profile"
1088 elif [ -f "$HOME/.zshrc" ]; then
1089 DETECTED_PROFILE="$HOME/.zshrc"
1090 fi
1091 fi
1092
1093 if [ ! -z "$DETECTED_PROFILE" ]; then
1094 echo "$DETECTED_PROFILE"
1095 fi
1096 }
1097
1098 _initconf() {
1099 _initpath
1100 if [ ! -f "$ACCOUNT_CONF_PATH" ] ; then
1101 echo "#Account configurations:
1102 #Here are the supported macros, uncomment them to make them take effect.
1103 #ACCOUNT_EMAIL=aaa@aaa.com # the account email used to register account.
1104
1105 #STAGE=1 # Use the staging api
1106 #FORCE=1 # Force to issue cert
1107 #DEBUG=1 # Debug mode
1108
1109 #dns api
1110 #######################
1111 #Cloudflare:
1112 #api key
1113 #CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
1114 #account email
1115 #CF_Email="xxxx@sss.com"
1116
1117 #######################
1118 #Dnspod.cn:
1119 #api key id
1120 #DP_Id="1234"
1121 #api key
1122 #DP_Key="sADDsdasdgdsf"
1123
1124 #######################
1125 #Cloudxns.com:
1126 #CX_Key="1234"
1127 #
1128 #CX_Secret="sADDsdasdgdsf"
1129
1130 " > $ACCOUNT_CONF_PATH
1131 fi
1132 }
1133
1134 install() {
1135 _initpath
1136
1137 if command -v yum > /dev/null ; then
1138 YUM="1"
1139 INSTALL="$SUDO yum install -y "
1140 elif command -v apt-get > /dev/null ; then
1141 INSTALL="$SUDO apt-get install -y "
1142 fi
1143
1144 if ! command -v "curl" > /dev/null ; then
1145 _err "Please install curl first."
1146 _err "$INSTALL curl"
1147 return 1
1148 fi
1149
1150 if ! command -v "crontab" > /dev/null ; then
1151 _err "Please install crontab first."
1152 if [ "$YUM" ] ; then
1153 _err "$INSTALL crontabs"
1154 else
1155 _err "$INSTALL crontab"
1156 fi
1157 return 1
1158 fi
1159
1160 if ! command -v "openssl" > /dev/null ; then
1161 _err "Please install openssl first."
1162 _err "$INSTALL openssl"
1163 return 1
1164 fi
1165
1166 _info "Installing to $LE_WORKING_DIR"
1167
1168 _info "Installed to $LE_WORKING_DIR/le.sh"
1169 cp le.sh $LE_WORKING_DIR/
1170 chmod +x $LE_WORKING_DIR/le.sh
1171
1172 _profile="$(_detect_profile)"
1173 if [ "$_profile" ] ; then
1174 _debug "Found profile: $_profile"
1175
1176 echo "LE_WORKING_DIR=$LE_WORKING_DIR
1177 alias le=\"$LE_WORKING_DIR/le.sh\"
1178 alias le.sh=\"$LE_WORKING_DIR/le.sh\"
1179 " > "$LE_WORKING_DIR/le.env"
1180
1181 _setopt "$_profile" "source \"$LE_WORKING_DIR/le.env\""
1182 _info "OK, Close and reopen your terminal to start using le"
1183 else
1184 _info "No profile is found, you will need to go into $LE_WORKING_DIR to use le.sh"
1185 fi
1186
1187 mkdir -p $LE_WORKING_DIR/dnsapi
1188 cp dnsapi/* $LE_WORKING_DIR/dnsapi/
1189
1190 #to keep compatible mv the .acc file to .key file
1191 if [ -f "$LE_WORKING_DIR/account.acc" ] ; then
1192 mv "$LE_WORKING_DIR/account.acc" "$LE_WORKING_DIR/account.key"
1193 fi
1194
1195 installcronjob
1196
1197 if [ ! -f "$ACCOUNT_CONF_PATH" ] ; then
1198 _initconf
1199 fi
1200 _info OK
1201 }
1202
1203 uninstall() {
1204 uninstallcronjob
1205 _initpath
1206
1207 _profile="$(_detect_profile)"
1208 if [ "$_profile" ] ; then
1209 sed -i /le.env/d "$_profile"
1210 fi
1211
1212 rm -f $LE_WORKING_DIR/le.sh
1213 _info "The keys and certs are in $LE_WORKING_DIR, you can remove them by yourself."
1214
1215 }
1216
1217 cron() {
1218 renewAll
1219 }
1220
1221 version() {
1222 _info "$PROJECT"
1223 _info "v$VER"
1224 }
1225
1226 showhelp() {
1227 version
1228 echo "Usage: le.sh [command] ...[args]....
1229 Avalible commands:
1230
1231 install:
1232 Install le.sh to your system.
1233 issue:
1234 Issue a cert.
1235 installcert:
1236 Install the issued cert to apache/nginx or any other server.
1237 renew:
1238 Renew a cert.
1239 renewAll:
1240 Renew all the certs.
1241 uninstall:
1242 Uninstall le.sh, and uninstall the cron job.
1243 version:
1244 Show version info.
1245 installcronjob:
1246 Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job.
1247 uninstallcronjob:
1248 Uninstall the cron job. The 'uninstall' command can do this automatically.
1249 createAccountKey:
1250 Create an account private key, professional use.
1251 createDomainKey:
1252 Create an domain private key, professional use.
1253 createCSR:
1254 Create CSR , professional use.
1255 "
1256 }
1257
1258
1259 if [ -z "$1" ] ; then
1260 showhelp
1261 else
1262 "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9"
1263 fi