Last active
December 19, 2024 19:48
-
-
Save qrkourier/4f850053ab0c485937851490f2abc523 to your computer and use it in GitHub Desktop.
portable shell script verifies the server cert for a Ziti identity file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
# | |
## a POSIX-portable diagnostic for a Ziti identity's trust chain | |
# | |
# raise exceptions | |
set -e | |
set -u | |
# Default values | |
if [ -z "${ZITI_ALPN-}" ]; then | |
ZITI_ALPN="h2,http/1.1" | |
fi | |
if [ -z "${TMPDIR-}" ]; then | |
TMPDIR=$(mktemp -d) | |
fi | |
BASENAME=$(basename "$0") | |
if [ $# -gt 0 ] && [ -s "$1" ] | |
then | |
ZITI_IDENTITY=$1 | |
elif [ -n "${ZITI_IDENTITY:-}" ] && [ -s "${ZITI_IDENTITY}" ] | |
then | |
: | |
else | |
printf ' | |
Usage: | |
\t%s [ZITI_IDENTITY] | |
Example: | |
\t%s /opt/openziti/etc/identities/ziti_id.json | |
Options: | |
\tZITI_ALPN\tALPN protocols to use when connecting to the controller | |
\t\t\t[h2,http/1.1|ziti-ctrl] (default: %s) | |
' "$BASENAME" "$BASENAME" "$ZITI_ALPN" | |
exit 1 | |
fi | |
get_ziti_url() { | |
if [ $# -eq 1 ] && [ -s "$1" ] | |
then | |
_ziti_id_file="$1" | |
else | |
echo "ERROR: get_ziti_url needs path to Ziti Identity JSON file as only param" >&2 | |
return 1 | |
fi | |
if command -v ${BIN_JQ-jq} >/dev/null 2>&1 | |
then | |
_ztAPI=$(jq -r ' | |
(if type == "string" then fromjson else . end) as $input | | |
($input.ztAPI | type) as $ztapi_type | | |
($input.ztAPI | if type == "string" then length else 0 end) as $ztapi_len | | |
($input.ztAPIs | type) as $ztapis_type | | |
($input.ztAPIs | if type == "array" then length else 0 end) as $ztapis_len | | |
($input.ztAPIs[0] | type) as $first_type | | |
if $ztapi_type == "string" and $ztapi_len > 0 | |
then $input.ztAPI | |
elif $ztapis_type == "array" and $ztapis_len > 0 and $first_type == "string" | |
then $input.ztAPIs[0] | |
else empty | |
end' "$_ziti_id_file") | |
elif [ "$(echo '{}' | ${BIN_PY-python} -m json.tool 2>/dev/null)" = "{}" ] | |
then | |
_ztAPI=$(${BIN_PY-python} -c ' | |
import sys, json | |
try: | |
data = json.load(sys.stdin) | |
ztAPI = data.get("ztAPI") | |
if ztAPI: | |
print(ztAPI) | |
elif "ztAPIs" in data and isinstance(data["ztAPIs"], list) and data["ztAPIs"]: | |
print(data["ztAPIs"][0]) | |
else: | |
sys.exit(1) | |
except Exception as e: | |
sys.stderr.write("ERROR: " + str(e) + "\n") | |
sys.exit(1) | |
' < "$_ziti_id_file") | |
elif [ "$(${BIN_NODE-node} -e "console.log(JSON.parse(process.argv[1]))" '{}' 2>/dev/null)" = "{}" ] | |
then | |
_ztAPI=$(${BIN_NODE-node} -e ' | |
try { | |
const data = require("fs").readFileSync(process.argv[1], "utf8"); | |
const json = JSON.parse(data); | |
if (typeof json.ztAPI === "string") { | |
console.log(json.ztAPI); | |
} else if (Array.isArray(json.ztAPIs) && json.ztAPIs.length > 0 && typeof json.ztAPIs[0] === "string") { | |
console.log(json.ztAPIs[0]); | |
} else { | |
throw new Error("Missing or invalid ztAPI/ztAPIs properties"); | |
} | |
} catch (e) { | |
console.error("ERROR:", e.message); | |
process.exit(1); | |
}' "$_ziti_id_file") | |
else | |
echo "ERROR: failed to find a JSON parser; need jq, python, or node" >&2 | |
return 1 | |
fi | |
if [ -z "$_ztAPI" ] | |
then | |
echo "ERROR: failed to find a Ziti API URL" >&2 | |
return 1 | |
else | |
echo "$_ztAPI" | |
unset _ztAPI | |
fi | |
} | |
get_ziti_edge() { | |
if [ $# -eq 1 ] && [ -n "$1" ] && [ "$(printf '%s' "$1" | cut -c1-8)" = "https://" ] | |
then | |
_ztAPI="$1" | |
else | |
echo "ERROR: parse_ziti_url needs Ziti API URL as only param" >&2 | |
return 1 | |
fi | |
_ziti_edge=$(echo "$_ztAPI" | awk -F[/:] '{print ($4!="" && $5!="")?$4":"($5==""?"443":$5):"ERROR"}') | |
if [ "$(echo "$_ziti_edge" | awk -F: '{print NF}')" -ne 2 ] | |
then | |
echo "ERROR: failed to extract a host and port from $_ztAPI" >&2 | |
return 1 | |
fi | |
echo "$_ziti_edge" | |
} | |
get_server_chain() { | |
if [ $# -eq 1 ] && [ -n "$1" ] && [ "$(echo "$1" | awk -F[:] '{print NF}')" -eq 2 ] | |
then | |
_ziti_edge="$1" | |
else | |
echo "ERROR: get_server_chain needs ziti edge as only param like host:port" >&2 | |
return 1 | |
fi | |
openssl s_client \ | |
-alpn "${ZITI_ALPN}" \ | |
-connect "${_ziti_edge}" \ | |
-showcerts </dev/null 2>/dev/null \ | |
| openssl storeutl -certs /dev/stdin | |
} | |
get_server_leaf() { | |
if [ $# -eq 1 ] && [ -s "$1" ] | |
then | |
_server_chain="$1" | |
else | |
echo "ERROR: get_server_leaf needs path to server chain PEM file as only param" >&2 | |
return 1 | |
fi | |
openssl storeutl -certs "$_server_chain" \ | |
| openssl x509 -outform PEM | |
} | |
get_well_known() { | |
if [ $# -eq 1 ] && [ -n "$1" ] && [ "$(echo "$1" | awk -F[:] '{print NF}')" -eq 2 ] | |
then | |
_ziti_edge="$1" | |
else | |
echo "ERROR: get_well_known needs ziti edge as only param like host:port" >&2 | |
return 1 | |
fi | |
if command -v "${BIN_CURL-curl}" >/dev/null 2>&1 | |
then | |
_encoded_pkcs7=$(curl --silent --show-error --insecure "https://$_ziti_edge/.well-known/est/cacerts") | |
elif command -v "${BIN_WGET-wget}" >/dev/null 2>&1 | |
then | |
_encoded_pkcs7=$(wget --quiet --no-check-certificate --output-document=- "https://$_ziti_edge/.well-known/est/cacerts") | |
else | |
echo "ERROR: failed to find an HTTP client; need curl or wget" >&2 | |
return 1 | |
fi | |
if [ -n "$_encoded_pkcs7" ] | |
then | |
echo "$_encoded_pkcs7" \ | |
| base64 -d \ | |
| openssl pkcs7 -inform DER -outform PEM -print_certs | |
else | |
echo "ERROR: failed to fetch well-known .well-known/est/cacerts" >&2 | |
return 1 | |
fi | |
} | |
# shellcheck disable=SC2317 | |
summary() { | |
_server_leaf_dn=$( | |
openssl storeutl -certs -noout -text ./server-leaf.pem \ | |
| grep -E '(Subject|Issuer):' | |
) 2>/dev/null || true # ignore errors | |
if [ -n "$_server_leaf_dn" ] | |
then | |
echo "DEBUG: server leaf distinguished name:" | |
echo "$_server_leaf_dn" | |
fi | |
_server_chain_dn=$( | |
openssl storeutl -certs -noout -text ./server-chain.pem \ | |
| grep -E '(Subject|Issuer):' | |
) 2>/dev/null || true # ignore errors | |
if [ -n "$_server_chain_dn" ] | |
then | |
echo "DEBUG: server chain distinguished names:" | |
echo "$_server_chain_dn" | |
fi | |
_well_known_dn=$( | |
openssl storeutl -certs -noout -text ./well-known.pem \ | |
| grep -E '(Subject|Issuer):' | |
) | |
if [ -n "$_well_known_dn" ] | |
then | |
echo "DEBUG: well-known distinguished names:" | |
echo "$_well_known_dn" | |
fi | |
echo "DEBUG: temporary files:" | |
if command -v tree >/dev/null 2>&1 | |
then | |
_tmp_files=$(tree "$(pwd)") 2>/dev/null || true # ignore errors | |
else | |
_tmp_files=$(ls -lARh "$(pwd)") 2>/dev/null || true # ignore errors | |
fi | |
if [ -n "$_tmp_files" ] | |
then | |
echo "$_tmp_files" | |
fi | |
} | |
# shellcheck disable=SC2317 | |
cleanup() { | |
_es=$? | |
if [ "$_es" -ne 0 ]; then | |
summary | |
fi | |
exit "$_es" | |
} | |
_bins="openssl base64 awk grep" | |
for _bin in $_bins | |
do | |
if ! command -v "$_bin" >/dev/null 2>&1 | |
then | |
echo "ERROR: failed to find $_bin (need $_bins)" >&2 | |
exit 1 | |
fi | |
done | |
cd "${TMPDIR}" | |
trap cleanup EXIT | |
ZITI_URL=$(get_ziti_url "${ZITI_IDENTITY-$1}") | |
ZITI_EDGE=$(get_ziti_edge "$ZITI_URL") | |
get_well_known "$ZITI_EDGE" > ./well-known.pem | |
get_server_chain "$ZITI_EDGE" > ./server-chain.pem | |
get_server_leaf ./server-chain.pem > ./server-leaf.pem | |
if openssl verify \ | |
-CAfile ./well-known.pem \ | |
-untrusted ./server-leaf.pem \ | |
./server-leaf.pem >/dev/null 2>&1 | |
then | |
echo 'INFO: verified by root CA' | |
exit 0 | |
elif openssl verify \ | |
-partial_chain \ | |
-CAfile ./well-known.pem \ | |
-untrusted ./server-leaf.pem \ | |
./server-leaf.pem >/dev/null 2>&1 | |
then | |
echo 'ERROR: partially verified by intermediate CA, missing root CA' >&2 | |
exit 1 | |
else | |
echo 'ERROR: not verified' >&2 | |
exit 1 | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment