diff --git a/README.md b/README.md index a1ebd8a..0f9e684 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,20 @@ acme.sh --issue --standalone -d aa.com -d www.aa.com -d cp.aa.com More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert +# Use Standalone tls server to issue cert + +**(requires you be root/sudoer, or you have permission to listen tcp 443 port)** + +acme.sh supports `tls-sni-01` validation. + +The tcp `443` port **MUST** be free to listen, otherwise you will be prompted to free the `443` port and try again. + +```bash +acme.sh --issue --tls -d aa.com -d www.aa.com -d cp.aa.com +``` + +More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert + # Use Apache mode **(requires you be root/sudoer, since it is required to interact with apache server)** diff --git a/acme.sh b/acme.sh index d7b3659..b08f885 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=2.2.6 +VER=2.2.7 PROJECT_NAME="acme.sh" @@ -17,6 +17,10 @@ STAGE_CA="https://acme-staging.api.letsencrypt.org" VTYPE_HTTP="http-01" VTYPE_DNS="dns-01" +VTYPE_TLS="tls-sni-01" +VTYPE_TLS2="tls-sni-02" + +W_TLS="tls" BEGIN_CSR="-----BEGIN CERTIFICATE REQUEST-----" END_CSR="-----END CERTIFICATE REQUEST-----" @@ -251,7 +255,7 @@ _dbase64() { fi } -#Usage: hashalg +#Usage: hashalg [outputhex] #Output Base64-encoded digest _digest() { alg="$1" @@ -260,8 +264,14 @@ _digest() { return 1 fi + outputhex="$2" + if [ "$alg" = "sha256" ] ; then - openssl dgst -sha256 -binary | _base64 + if [ "$outputhex" ] ; then + echo $(openssl dgst -sha256 -hex | cut -d = -f 2) + else + openssl dgst -sha256 -binary | _base64 + fi else _err "$alg is not supported yet" return 1 @@ -288,6 +298,86 @@ _sign() { } +# _createkey 2048|ec-256 file +_createkey() { + length="$1" + f="$2" + isec="" + if _startswith "$length" "ec-" ; then + isec="1" + length=$(printf $length | cut -d '-' -f 2-100) + eccname="$length" + fi + + if [ -z "$length" ] ; then + if [ "$isec" ] ; then + length=256 + else + length=2048 + fi + fi + _info "Use length $length" + + if [ "$isec" ] ; then + if [ "$length" = "256" ] ; then + eccname="prime256v1" + fi + if [ "$length" = "384" ] ; then + eccname="secp384r1" + fi + if [ "$length" = "521" ] ; then + eccname="secp521r1" + fi + _info "Using ec name: $eccname" + fi + + #generate account key + if [ "$isec" ] ; then + openssl ecparam -name $eccname -genkey 2>/dev/null > "$f" + else + openssl genrsa $length 2>/dev/null > "$f" + fi +} + +#_createcsr cn san_list keyfile csrfile conf +_createcsr() { + _debug _createcsr + domain="$1" + domainlist="$2" + key="$3" + csr="$4" + csrconf="$5" + _debug2 domain "$domain" + _debug2 domainlist "$domainlist" + if [ -z "$domainlist" ] || [ "$domainlist" = "no" ]; then + #single domain + _info "Single domain" "$domain" + printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\n" > "$csrconf" + openssl req -new -sha256 -key "$key" -subj "/CN=$domain" -config "$csrconf" -out "$csr" + else + if _contains "$domainlist" "," ; then + alt="DNS:$(echo $domainlist | sed "s/,/,DNS:/g")" + else + alt="DNS:$domainlist" + fi + #multi + _info "Multi domain" "$alt" + printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment\nsubjectAltName=$alt" > "$csrconf" + openssl req -new -sha256 -key "$key" -subj "/CN=$domain" -config "$csrconf" -out "$csr" + fi +} + +#_signcsr key csr conf cert +_signcsr() { + key="$1" + csr="$2" + conf="$3" + cert="$4" + + openssl x509 -req -days 365 -in "$csr" -signkey "$key" -extensions v3_req -extfile "$conf" -out "$cert" + +} + _ss() { _port="$1" @@ -364,7 +454,7 @@ createAccountKey() { return else #generate account key - openssl genrsa $length 2>/dev/null > "$ACCOUNT_KEY_PATH" + _createkey $length "$ACCOUNT_KEY_PATH" fi } @@ -378,45 +468,12 @@ createDomainKey() { fi domain=$1 - length=$2 - isec="" - if _startswith "$length" "ec-" ; then - isec="1" - length=$(printf $length | cut -d '-' -f 2-100) - eccname="$length" - fi - - if [ -z "$length" ] ; then - if [ "$isec" ] ; then - length=256 - else - length=2048 - fi - fi - _info "Use length $length" - - if [ "$isec" ] ; then - if [ "$length" = "256" ] ; then - eccname="prime256v1" - fi - if [ "$length" = "384" ] ; then - eccname="secp384r1" - fi - if [ "$length" = "521" ] ; then - eccname="secp521r1" - fi - _info "Using ec name: $eccname" - fi - _initpath $domain + length=$2 + if [ ! -f "$CERT_KEY_PATH" ] || ( [ "$FORCE" ] && ! [ "$IS_RENEW" ] ); then - #generate account key - if [ "$isec" ] ; then - openssl ecparam -name $eccname -genkey 2>/dev/null > "$CERT_KEY_PATH" - else - openssl genrsa $length 2>/dev/null > "$CERT_KEY_PATH" - fi + _createkey "$length" "$CERT_KEY_PATH" else if [ "$IS_RENEW" ] ; then _info "Domain key exists, skip" @@ -447,19 +504,8 @@ createCSR() { return fi - if [ -z "$domainlist" ] || [ "$domainlist" = "no" ]; then - #single domain - _info "Single domain" "$domain" - printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\n" > "$DOMAIN_SSL_CONF" - openssl req -new -sha256 -key "$CERT_KEY_PATH" -subj "/CN=$domain" -config "$DOMAIN_SSL_CONF" -out "$CSR_PATH" - else - alt="DNS:$(echo $domainlist | sed "s/,/,DNS:/g")" - #multi - _info "Multi domain" "$alt" - printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\n[SAN]\nsubjectAltName=$alt" > "$DOMAIN_SSL_CONF" - openssl req -new -sha256 -key "$CERT_KEY_PATH" -subj "/CN=$domain" -reqexts SAN -config "$DOMAIN_SSL_CONF" -out "$CSR_PATH" - fi - + _createcsr "$domain" "$domainlist" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" + } _urlencode() { @@ -819,11 +865,54 @@ _stopserver(){ if [ -z "$pid" ] ; then return fi - + _get "http://localhost:$Le_HTTPPort" >/dev/null 2>&1 + _get "http://localhost:$Le_TLSPort" >/dev/null 2>&1 } + +# _starttlsserver san_a san_b port content +_starttlsserver() { + _info "Starting tls server." + san_a="$1" + san_b="$2" + port="$3" + content="$4" + + _debug san_a "$san_a" + _debug san_b "$san_b" + _debug port "$port" + + #create key TLS_KEY + if ! _createkey "2048" "$TLS_KEY" ; then + _err "Create tls validation key error." + return 1 + fi + + #create csr + alt="$san_a" + if [ "$san_b" ] ; then + alt="$alt,$san_b" + fi + if ! _createcsr "tls.acme.sh" "$alt" "$TLS_KEY" "$TLS_CSR" "$TLS_CONF" ; then + _err "Create tls validation csr error." + return 1 + fi + + #self signed + if ! _signcsr "$TLS_KEY" "$TLS_CSR" "$TLS_CONF" "$TLS_CERT" ; then + _err "Create tls validation cert error." + return 1 + fi + + #start openssl + (printf "HTTP/1.1 200 OK\r\n\r\n$content" | openssl s_server -cert "$TLS_CERT" -key "$TLS_KEY" -accept $port >/dev/null 2>&1) & + serverproc="$!" + sleep 2 + _debug serverproc $serverproc +} + _initpath() { if [ -z "$LE_WORKING_DIR" ] ; then @@ -935,6 +1024,20 @@ _initpath() { if [ -z "$CERT_PFX_PATH" ] ; then CERT_PFX_PATH="$domainhome/$domain.pfx" fi + + if [ -z "$TLS_CONF" ] ; then + TLS_CONF="$domainhome/tls.valdation.conf" + fi + if [ -z "$TLS_CERT" ] ; then + TLS_CERT="$domainhome/tls.valdation.cert" + fi + if [ -z "$TLS_KEY" ] ; then + TLS_KEY="$domainhome/tls.valdation.key" + fi + if [ -z "$TLS_CSR" ] ; then + TLS_CSR="$domainhome/tls.valdation.csr" + fi + } @@ -1073,6 +1176,12 @@ _clearup() { _stopserver $serverproc serverproc="" _restoreApache + if [ -z "$DEBUG" ] ; then + rm -f "$TLS_CONF" + rm -f "$TLS_CERT" + rm -f "$TLS_KEY" + rm -f "$TLS_CSR" + fi } # webroot removelevel tokenfile @@ -1171,6 +1280,24 @@ issue() { fi fi + if _hasfield "$Le_Webroot" "$W_TLS" ; then + _info "Standalone tls mode." + + if [ -z "$Le_TLSPort" ] ; then + Le_TLSPort=443 + else + _savedomainconf "Le_TLSPort" "$Le_TLSPort" + fi + + netprc="$(_ss "$Le_TLSPort" | grep "$Le_TLSPort")" + if [ "$netprc" ] ; then + _err "$netprc" + _err "tcp port $Le_TLSPort is already used by $(echo "$netprc" | cut -d : -f 4)" + _err "Please stop it first" + return 1 + fi + fi + if _hasfield "$Le_Webroot" "apache" ; then if ! _setApache ; then _err "set up apache error. Report error to me." @@ -1263,6 +1390,11 @@ issue() { if _startswith "$_currentRoot" "dns" ; then vtype="$VTYPE_DNS" fi + + if [ "$_currentRoot" = "$W_TLS" ] ; then + vtype="$VTYPE_TLS" + fi + _info "Getting token for domain" $d if ! _send_signed_request "$API/acme/new-authz" "{\"resource\": \"new-authz\", \"identifier\": {\"type\": \"dns\", \"value\": \"$d\"}}" ; then @@ -1406,7 +1538,7 @@ issue() { _debug "keyauthorization" "$keyauthorization" _debug "uri" "$uri" removelevel="" - token="" + token="$(printf "%s" "$keyauthorization" | cut -d '.' -f 1)" _debug "_currentRoot" "$_currentRoot" @@ -1439,7 +1571,6 @@ issue() { _debug wellknown_path "$wellknown_path" - token="$(printf "%s" "$keyauthorization" | cut -d '.' -f 1)" _debug "writing token:$token to $wellknown_path/$token" mkdir -p "$wellknown_path" @@ -1451,6 +1582,37 @@ issue() { fi fi + + elif [ "$vtype" = "$VTYPE_TLS" ] ; then + #create A + #_hash_A="$(printf "%s" $token | _digest "sha256" "hex" )" + #_debug2 _hash_A "$_hash_A" + #_x="$(echo $_hash_A | cut -c 1-32)" + #_debug2 _x "$_x" + #_y="$(echo $_hash_A | cut -c 33-64)" + #_debug2 _y "$_y" + #_SAN_A="$_x.$_y.token.acme.invalid" + #_debug2 _SAN_A "$_SAN_A" + + #create B + _hash_B="$(printf "%s" $keyauthorization | _digest "sha256" "hex" )" + _debug2 _hash_B "$_hash_B" + _x="$(echo $_hash_B | cut -c 1-32)" + _debug2 _x "$_x" + _y="$(echo $_hash_B | cut -c 33-64)" + _debug2 _y "$_y" + + #_SAN_B="$_x.$_y.ka.acme.invalid" + + _SAN_B="$_x.$_y.acme.invalid" + _debug2 _SAN_B "$_SAN_B" + + if ! _starttlsserver "$_SAN_B" "$_SAN_A" "$Le_TLSPort" "$keyauthorization" ; then + _err "Start tls server error." + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + return 1 + fi fi if ! _send_signed_request $uri "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}" ; then @@ -2203,6 +2365,7 @@ Parameters: --webroot, -w /path/to/webroot Specifies the web root folder for web root mode. --standalone Use standalone mode. + --tls Use standalone tls mode. --apache Use apache mode. --dns [dns_cf|dns_dp|dns_cx|/path/to/api/file] Use dns mode or dns api. --dnssleep [60] The time in seconds to wait for all the txt records to take effect in dns api mode. Default 60 seconds. @@ -2227,6 +2390,7 @@ Parameters: --accountkey Specifies the account key path, Only valid for the '--install' command. --days Specifies the days to renew the cert when using '--issue' command. The max value is 80 days. --httpport Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer. + --tlsport Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer. --listraw Only used for '--list' command, list the certs in raw format. " } @@ -2277,6 +2441,7 @@ _process() { _accountkey="" _certhome="" _httpport="" + _tlsport="" _dnssleep="" _listraw="" while [ ${#} -gt 0 ] ; do @@ -2399,6 +2564,14 @@ _process() { _webroot="$_webroot,$wvalue" fi ;; + --tls) + wvalue="$W_TLS" + if [ -z "$_webroot" ] ; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; --dns) wvalue="dns" if ! _startswith "$2" "-" ; then @@ -2490,6 +2663,12 @@ _process() { Le_HTTPPort="$_httpport" shift ;; + --tlsport ) + _tlsport="$2" + Le_TLSPort="$_tlsport" + shift + ;; + --listraw ) _listraw="raw" ;;