You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

424 lines
17 KiB

  1. #!/usr/bin/env sh
  2. # Deploy certificates to Zyxel GS1900 series switches
  3. #
  4. # This script uses the https web administration interface in order
  5. # to upload updated certificates to Zyxel GS1900 series switches.
  6. # Only a few models have been tested but untested switches from the
  7. # same model line may work as well. If you test and confirm a switch
  8. # as working please submit a pull request updating this compatibility
  9. # list!
  10. #
  11. # Known Issues:
  12. # 1. This is a consumer grade switch and is a bit underpowered
  13. # the longer the RSA key size the slower your switch web UI
  14. # will be. RSA 2048 will work, RSA 4096 will work but you may
  15. # experience performance problems.
  16. # 2. You must use RSA certificates. The switch will reject EC-256
  17. # and EC-384 certificates in firmware 2.80
  18. # See: https://community.zyxel.com/en/discussion/21506/bug-cannot-import-ssl-cert-on-gs1900-8-and-gs1900-24e-firmware-v2-80/
  19. #
  20. # Current GS1900 Switch Compatibility:
  21. # GS1900-8 - Working as of firmware V2.80
  22. # GS1900-8HP - Untested
  23. # GS1900-10HP - Untested
  24. # GS1900-16 - Untested
  25. # GS1900-24 - Untested
  26. # GS1900-24E - Working as of firmware V2.80
  27. # GS1900-24EP - Untested
  28. # GS1900-24HP - Untested
  29. # GS1900-48 - Untested
  30. # GS1900-48HP - Untested
  31. #
  32. # Prerequisite Setup Steps:
  33. # 1. Install at least firmware V2.80 on your switch
  34. # 2. Enable HTTPS web management on your switch
  35. #
  36. # Usage:
  37. # 1. Ensure the switch has firmware V2.80 or later.
  38. # 2. Ensure the switch has HTTPS management enabled.
  39. # 3. Set the appropriate environment variables for your environment.
  40. #
  41. # DEPLOY_ZYXEL_SWITCH - The switch hostname. (Default: _cdomain)
  42. # DEPLOY_ZYXEL_SWITCH_USER - The webadmin user. (Default: admin)
  43. # DEPLOY_ZYXEL_SWITCH_PASSWORD - The webadmin password for the switch.
  44. # DEPLOY_ZYXEL_SWITCH_REBOOT - If "1" reboot after update. (Default: "0")
  45. #
  46. # 4. Run the deployment plugin:
  47. # acme.sh --deploy --deploy-hook zyxel_gs1900 -d example.com
  48. #
  49. # returns 0 means success, otherwise error.
  50. #domain keyfile certfile cafile fullchain
  51. zyxel_gs1900_deploy() {
  52. _zyxel_gs1900_minimum_firmware_version="v2.80"
  53. _cdomain="$1"
  54. _ckey="$2"
  55. _ccert="$3"
  56. _cca="$4"
  57. _cfullchain="$5"
  58. _debug _cdomain "$_cdomain"
  59. _debug2 _ckey "$_ckey"
  60. _debug _ccert "$_ccert"
  61. _debug _cca "$_cca"
  62. _debug _cfullchain "$_cfullchain"
  63. _getdeployconf DEPLOY_ZYXEL_SWITCH
  64. if [ -z "$DEPLOY_ZYXEL_SWITCH" ]; then
  65. _zyxel_switch_host="$_cdomain"
  66. else
  67. _zyxel_switch_host="$DEPLOY_ZYXEL_SWITCH"
  68. _savedeployconf DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH"
  69. fi
  70. _debug2 DEPLOY_ZYXEL_SWITCH "$_zyxel_switch_host"
  71. _getdeployconf DEPLOY_ZYXEL_SWITCH_USER
  72. if [ -z "$DEPLOY_ZYXEL_SWITCH_USER" ]; then
  73. _zyxel_switch_user="admin"
  74. else
  75. _zyxel_switch_user="$DEPLOY_ZYXEL_SWITCH_USER"
  76. _savedeployconf DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER"
  77. fi
  78. _debug2 DEPLOY_ZYXEL_SWITCH_USER "$_zyxel_switch_user"
  79. _getdeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD
  80. if [ -z "$DEPLOY_ZYXEL_SWITCH_PASSWORD" ]; then
  81. _zyxel_switch_password="1234"
  82. else
  83. _zyxel_switch_password="$DEPLOY_ZYXEL_SWITCH_PASSWORD"
  84. _savedeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD"
  85. fi
  86. _secure_debug2 DEPLOY_ZYXEL_SWITCH_PASSWORD "$_zyxel_switch_password"
  87. _getdeployconf DEPLOY_ZYXEL_SWITCH_REBOOT
  88. if [ -z "$DEPLOY_ZYXEL_SWITCH_REBOOT" ]; then
  89. _zyxel_switch_reboot="0"
  90. else
  91. _zyxel_switch_reboot="$DEPLOY_ZYXEL_SWITCH_REBOOT"
  92. _savedeployconf DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT"
  93. fi
  94. _debug2 DEPLOY_ZYXEL_SWITCH_REBOOT "$_zyxel_switch_reboot"
  95. _zyxel_switch_base_uri="https://${_zyxel_switch_host}"
  96. _info "Beginning to deploy to a Zyxel GS1900 series switch at ${_zyxel_switch_base_uri}."
  97. _zyxel_gs1900_deployment_precheck || return $?
  98. _info "Logging into the switch web interface."
  99. _zyxel_gs1900_login || return $?
  100. _info "Validating the switch is compatible with this deployment process."
  101. _zyxel_gs1900_validate_device_compatibility || return $?
  102. _info "Uploading the certificate."
  103. _zyxel_gs1900_upload_certificate || return $?
  104. if [ "$_zyxel_switch_reboot" = "1" ]; then
  105. _info "Rebooting the switch."
  106. _zyxel_gs1900_trigger_reboot || return $?
  107. fi
  108. return 0
  109. }
  110. _zyxel_gs1900_deployment_precheck() {
  111. # Initialize the keylength if it isn't already
  112. if [ -z "$Le_Keylength" ]; then
  113. Le_Keylength=""
  114. fi
  115. if _isEccKey "$Le_Keylength"; then
  116. _info "Warning: Zyxel GS1900 switches are not currently known to work with ECC keys!"
  117. _info "You can continue, but your switch may reject your key."
  118. elif [ -n "$Le_Keylength" ] && [ "$Le_Keylength" -gt "2048" ]; then
  119. _info "Warning: Your RSA key length is greater than 2048!"
  120. _info "You can continue, but you may experience performance issues in the web administration interface."
  121. fi
  122. # Check the server for some common failure modes prior to authentication and certificate upload in order to avoid
  123. # sending a certificate when we may not want to.
  124. _post "username=test&password=test&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi" '' "POST" "application/x-www-form-urlencoded" >/dev/null 2>&1
  125. test_login_page_exitcode="$?"
  126. if [ "$test_login_page_exitcode" -ne "0" ]; then
  127. if [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "56" ]; then
  128. _info "Warning: curl is returning exit code 56. Please re-run with --debug for more information."
  129. _debug "If the above curl trace contains the error 'SSL routines::unexpected eof while reading, errno 0'"
  130. _debug "please ensure you are running the latest versions of curl and openssl. For more information"
  131. _debug "see: https://github.com/openssl/openssl/issues/18866#issuecomment-1194219601"
  132. elif { [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "60" ]; } || { [ "${ACME_USE_WGET:-0}" = "1" ] && [ "$test_login_page_exitcode" = "5" ]; }; then
  133. _err "The SSL certificate at $_zyxel_switch_base_uri could not be validated."
  134. _err "Please double check your hostname, port, and that you are actually connecting to your switch."
  135. _err "If the problem persists then please ensure that the certificate is not self-signed, has not"
  136. _err "expired, and matches the switch hostname. If you expect validation to fail then you can disable"
  137. _err "certificate validation by running with --insecure."
  138. return 1
  139. else
  140. _err "Failed to submit the initial login attempt to $_zyxel_switch_base_uri."
  141. return 1
  142. fi
  143. fi
  144. }
  145. _zyxel_gs1900_login() {
  146. # Login to the switch and set the appropriate auth cookie in _H1
  147. username_encoded=$(printf "%s" "$_zyxel_switch_user" | _url_encode)
  148. password_encoded=$(_zyxel_gs1900_password_obfuscate "$_zyxel_switch_password" | _url_encode)
  149. login_response=$(_post "username=${username_encoded}&password=${password_encoded}&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n')
  150. auth_response=$(_post "authId=${login_response}&login_chk=true" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n')
  151. if [ "$auth_response" != "OK" ]; then
  152. _err "Login failed due to invalid credentials."
  153. _err "Please double check the configured username and password and try again."
  154. return 1
  155. fi
  156. sessionid=$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'HTTPS_XSSID=[^;]*;' | tr -d ';')
  157. _secure_debug2 "sessionid" "$sessionid"
  158. export _H1="Cookie: $sessionid"
  159. _secure_debug2 "_H1" "$_H1"
  160. return 0
  161. }
  162. _zyxel_gs1900_validate_device_compatibility() {
  163. # Check the switches model and firmware version and throw errors
  164. # if this script isn't compatible.
  165. device_info_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=12" | tr -d '\n')
  166. model_name=$(_zyxel_gs1900_get_model "$device_info_html")
  167. _debug2 "model_name" "$model_name"
  168. if [ -z "$model_name" ]; then
  169. _err "Could not find the switch model name."
  170. _err "Please re-run with --debug and report a bug."
  171. return $?
  172. fi
  173. if ! expr "$model_name" : "GS1900-" >/dev/null; then
  174. _err "Switch is an unsupported model: $model_name"
  175. return 1
  176. fi
  177. firmware_version=$(_zyxel_gs1900_get_firmware_version "$device_info_html")
  178. _debug2 "firmware_version" "$firmware_version"
  179. if [ -z "$firmware_version" ]; then
  180. _err "Could not find the switch firmware version."
  181. _err "Please re-run with --debug and report a bug."
  182. return $?
  183. fi
  184. _debug2 "_zyxel_gs1900_minimum_firmware_version" "$_zyxel_gs1900_minimum_firmware_version"
  185. minimum_major_version=$(_zyxel_gs1900_parse_major_version "$_zyxel_gs1900_minimum_firmware_version")
  186. _debug2 "minimum_major_version" "$minimum_major_version"
  187. minimum_minor_version=$(_zyxel_gs1900_parse_minor_version "$_zyxel_gs1900_minimum_firmware_version")
  188. _debug2 "minimum_minor_version" "$minimum_minor_version"
  189. _debug2 "firmware_version" "$firmware_version"
  190. firmware_major_version=$(_zyxel_gs1900_parse_major_version "$firmware_version")
  191. _debug2 "firmware_major_version" "$firmware_major_version"
  192. firmware_minor_version=$(_zyxel_gs1900_parse_minor_version "$firmware_version")
  193. _debug2 "firmware_minor_version" "$firmware_minor_version"
  194. _ret=0
  195. if [ "$firmware_major_version" -lt "$minimum_major_version" ]; then
  196. _ret=1
  197. elif [ "$firmware_major_version" -eq "$minimum_major_version" ] && [ "$firmware_minor_version" -lt "$minimum_minor_version" ]; then
  198. _ret=1
  199. fi
  200. if [ "$_ret" != "0" ]; then
  201. _err "Unsupported firmware version $firmware_version. Please upgrade to at least version $_zyxel_gs1900_minimum_firmware_version."
  202. fi
  203. return $?
  204. }
  205. _zyxel_gs1900_upload_certificate() {
  206. # Generate a PKCS12 certificate with a temporary password since the web interface
  207. # requires a password be present. Then upload that certificate.
  208. temp_cert_password=$(head /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 64)
  209. _secure_debug2 "temp_cert_password" "$temp_cert_password"
  210. temp_pkcs12="$(_mktemp)"
  211. _debug2 "temp_pkcs12" "$temp_pkcs12"
  212. _toPkcs "$temp_pkcs12" "$_ckey" "$_ccert" "$_cca" "$temp_cert_password"
  213. if [ "$?" != "0" ]; then
  214. _err "Failed to generate a pkcs12 certificate."
  215. _err "Please re-run with --debug and report a bug."
  216. # ensure the temporary certificate file is cleaned up
  217. [ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}"
  218. return $?
  219. fi
  220. upload_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5914" | tr -d '\n')
  221. # Get the two validity dates by looking for their date format in the page (i.e. Mar 5 05:48:48 2024 GMT)
  222. existing_validity=$(_zyxel_html_extract_dates "$upload_page_html")
  223. _debug2 "existing_validity" "$existing_validity"
  224. form_xss_value=$(printf "%s" "$upload_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g')
  225. _secure_debug2 "form_xss_value" "$form_xss_value"
  226. # If a certificate exists on the switch already there will be two XSS keys - we want the first one
  227. form_xss_value=$(printf "%s" "$form_xss_value" | head -n 1)
  228. _secure_debug2 "form_xss_value" "$form_xss_value"
  229. _info "Generating the certificate upload request"
  230. upload_post_request="$(_mktemp)"
  231. upload_post_boundary="---------------------------$(date +%Y%m%d%H%M%S)"
  232. {
  233. printf -- "--%s\r\n" "${upload_post_boundary}"
  234. printf "Content-Disposition: form-data; name=\"XSSID\"\r\n\r\n%s\r\n" "${form_xss_value}"
  235. printf -- "--%s\r\n" "${upload_post_boundary}"
  236. printf "Content-Disposition: form-data; name=\"http_file\"; filename=\"temp_pkcs12.pfx\"\r\n"
  237. printf "Content-Type: application/pkcs12\r\n\r\n"
  238. cat "${temp_pkcs12}"
  239. printf "\r\n"
  240. printf -- "--%s\r\n" "${upload_post_boundary}"
  241. printf "Content-Disposition: form-data; name=\"pwd\"\r\n\r\n%s\r\n" "${temp_cert_password}"
  242. printf -- "--%s\r\n" "${upload_post_boundary}"
  243. printf "Content-Disposition: form-data; name=\"cmd\"\r\n\r\n%s\r\n" "31"
  244. printf -- "--%s\r\n" "${upload_post_boundary}"
  245. printf "Content-Disposition: form-data; name=\"sysSubmit\"\r\n\r\n%s\r\n" "Import"
  246. printf -- "--%s--\r\n" "${upload_post_boundary}"
  247. } >"${upload_post_request}"
  248. _info "Upload certificate to the switch"
  249. # Unfortunately we cannot rely upon the switch response across switch models
  250. # to return a consistent body return - so we cannot inspect the result of this
  251. # upload to determine success. We will need to re-query the certificates page
  252. # and compare the validity dates to try and identify if they have changed.
  253. _post "${upload_post_request}" "${_zyxel_switch_base_uri}/cgi-bin/httpuploadcert.cgi" '' "POST" "multipart/form-data; boundary=${upload_post_boundary}" '1' >/dev/null 2>&1
  254. rm "${upload_post_request}"
  255. # Pause for a few seconds to give the switch a chance to process the certificate
  256. # For some reason I've found this to be necessary on my GS1900-24E
  257. _debug2 "Waiting 4 seconds for the switch to process the newly uploaded certificate."
  258. sleep "4"
  259. _debug2 "Checking to see if the certificate updated properly"
  260. upload_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5914" | tr -d '\n')
  261. new_validity=$(_zyxel_html_extract_dates "$upload_page_html")
  262. _debug2 "new_validity" "$existing_validity"
  263. _ret=0
  264. if [ "$existing_validity" != "$new_validity" ]; then
  265. _debug2 "The certificate validity has changed. The upload must have succeeded."
  266. else
  267. _ret=1
  268. _err "The certificate upload does not appear to have worked."
  269. _err "Either the certificate provided has not changed, or the switch is returning an unexpected error."
  270. _err "Please re-run with --debug 2 and review for unexpected errors. If none can be found please submit a bug."
  271. fi
  272. # ensure the temporary files are cleaned up
  273. [ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}"
  274. return $_ret
  275. }
  276. _zyxel_gs1900_trigger_reboot() {
  277. # Trigger a reboot via the management reboot page in the web ui
  278. reboot_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5888" | tr -d '\n')
  279. reboot_xss_value=$(printf "%s" "$reboot_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g')
  280. _secure_debug2 "reboot_xss_value" "$reboot_xss_value"
  281. reboot_response_html=$(_post "XSSID=${reboot_xss_value}&cmd=5889&sysSubmit=Reboot" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi" '' "POST" "application/x-www-form-urlencoded")
  282. reboot_message=$(printf "%s" "$reboot_response_html" | tr -d '\t\r\n\v\f' | _egrep_o "Rebooting now...")
  283. if [ -z "$reboot_message" ]; then
  284. _err "Failed to trigger switch reboot!"
  285. return 1
  286. fi
  287. return 0
  288. }
  289. # html
  290. _zyxel_html_extract_dates() {
  291. html="$1"
  292. # Extract all dates in the html which match the format "Mar 5 05:48:48 2024 GMT"
  293. # Note that the number of spaces between the format sections may differ for some reason
  294. printf "%s" "$html" | _egrep_o '[A-Za-z]{3}\s+[0-9]+\s+[0-9]+:[0-9]+:[0-9]+\s+[0-9]+\s+[A-Za-z]+'
  295. }
  296. # html label
  297. _zyxel_html_table_lookup() {
  298. # Look up a value in the html representing the status page of the switch
  299. # when provided with the html of the page and the label (i.e. "Model Name:")
  300. html="$1"
  301. label=$(printf "%s" "$2" | tr -d ' ')
  302. lookup_result=$(printf "%s" "$html" | tr -d "\t\r\n\v\f" | sed 's/<tr>/\n<tr>/g' | sed 's/<td[^>]*>/<td>/g' | tr -d ' ' | grep -i "$label" | sed "s/<tr><td>$label<\/td><td>\([^<]\{1,\}\)<\/td><\/tr>/\1/i")
  303. printf "%s" "$lookup_result"
  304. return 0
  305. }
  306. # password
  307. _zyxel_gs1900_password_obfuscate() {
  308. # Return the password obfuscated via the same method used by the
  309. # Zyxel Web UI login process
  310. login_allowed_chrs="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  311. login_pw_arg="$1"
  312. login_pw_len="${#login_pw_arg}"
  313. login_pw_index="${#login_pw_arg}"
  314. login_pw_obfuscated=""
  315. i=1
  316. while [ "$i" -le "$(_math "321" - "$login_pw_index")" ]; do
  317. append_chr="0"
  318. if [ "$((i % 5))" -eq 0 ] && [ "$login_pw_index" -gt 0 ]; then
  319. login_pw_index=$(_math "$login_pw_index" - 1)
  320. append_chr=$(echo "$login_pw_arg" | awk -v var="$login_pw_index" '{ str=substr($0,var+1,1); print str }')
  321. elif [ "$i" -eq 123 ]; then
  322. if [ "${login_pw_len}" -lt 10 ]; then
  323. # The 123rd character must be 0 if the login_pw_arg is less than 10 characters
  324. append_chr="0"
  325. else
  326. # Or the login_pw_arg divided by 10 rounded down if greater than or equal to 10
  327. append_chr=$(_math "$login_pw_len" / 10)
  328. fi
  329. elif [ $i -eq 289 ]; then
  330. # the 289th character must be the len % 10
  331. append_chr=$(_math "$login_pw_len" % 10)
  332. else
  333. # add random characters for the sake of obfuscation...
  334. rand=$(head -q /dev/urandom | tr -cd '0-9' | head -c5 | sed 's/^0\{1,\}//')
  335. rand=$(printf "%5d" "$rand")
  336. rand_idx=$(_math "$rand" % "${#login_allowed_chrs}")
  337. append_chr=$(echo "$login_allowed_chrs" | awk -v var="$rand_idx" '{ str=substr($0,var+1,1); print str }')
  338. fi
  339. login_pw_obfuscated="${login_pw_obfuscated}${append_chr}"
  340. i=$(_math "$i" + 1)
  341. done
  342. printf "%s" "$login_pw_obfuscated"
  343. }
  344. _zyxel_gs1900_get_model() {
  345. html="$1"
  346. model_name=$(_zyxel_html_table_lookup "$html" "Model Name:")
  347. printf "%s" "$model_name"
  348. }
  349. _zyxel_gs1900_get_firmware_version() {
  350. html="$1"
  351. firmware_version=$(_zyxel_html_table_lookup "$html" "Firmware Version:" | _egrep_o "V[^.]+.[^(]+")
  352. printf "%s" "$firmware_version"
  353. }
  354. _zyxel_gs1900_parse_major_version() {
  355. printf "%s" "$1" | sed 's/^V\([0-9]\{1,\}\).\{1,\}$/\1/gi'
  356. }
  357. _zyxel_gs1900_parse_minor_version() {
  358. printf "%s" "$1" | sed 's/^.\{1,\}\.\([0-9]\{1,\}\)$/\1/gi'
  359. }